specweave 0.33.3 → 0.33.5

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 (106) hide show
  1. package/CLAUDE.md +85 -19
  2. package/dist/src/cli/cleanup-zombies.js +8 -5
  3. package/dist/src/cli/cleanup-zombies.js.map +1 -1
  4. package/dist/src/cli/commands/jobs.js +19 -2
  5. package/dist/src/cli/commands/jobs.js.map +1 -1
  6. package/dist/src/cli/commands/living-docs.js +1 -1
  7. package/dist/src/cli/commands/living-docs.js.map +1 -1
  8. package/dist/src/cli/helpers/init/external-import-grouping.d.ts.map +1 -1
  9. package/dist/src/cli/helpers/init/external-import-grouping.js +11 -7
  10. package/dist/src/cli/helpers/init/external-import-grouping.js.map +1 -1
  11. package/dist/src/cli/workers/clone-worker.js +22 -5
  12. package/dist/src/cli/workers/clone-worker.js.map +1 -1
  13. package/dist/src/config/types.d.ts +203 -1208
  14. package/dist/src/config/types.d.ts.map +1 -1
  15. package/dist/src/core/background/job-dependency.d.ts.map +1 -1
  16. package/dist/src/core/background/job-dependency.js +1 -0
  17. package/dist/src/core/background/job-dependency.js.map +1 -1
  18. package/dist/src/core/background/job-launcher.js +2 -2
  19. package/dist/src/core/background/job-launcher.js.map +1 -1
  20. package/dist/src/core/background/job-manager.d.ts +8 -0
  21. package/dist/src/core/background/job-manager.d.ts.map +1 -1
  22. package/dist/src/core/background/job-manager.js +19 -1
  23. package/dist/src/core/background/job-manager.js.map +1 -1
  24. package/dist/src/core/background/types.d.ts +9 -1
  25. package/dist/src/core/background/types.d.ts.map +1 -1
  26. package/dist/src/core/background/types.js +8 -1
  27. package/dist/src/core/background/types.js.map +1 -1
  28. package/dist/src/importers/external-importer.d.ts +26 -5
  29. package/dist/src/importers/external-importer.d.ts.map +1 -1
  30. package/dist/src/importers/item-converter.d.ts.map +1 -1
  31. package/dist/src/importers/item-converter.js +18 -1
  32. package/dist/src/importers/item-converter.js.map +1 -1
  33. package/dist/src/importers/jira-importer.d.ts +10 -0
  34. package/dist/src/importers/jira-importer.d.ts.map +1 -1
  35. package/dist/src/importers/jira-importer.js +70 -6
  36. package/dist/src/importers/jira-importer.js.map +1 -1
  37. package/dist/src/init/architecture/types.d.ts +33 -140
  38. package/dist/src/init/architecture/types.d.ts.map +1 -1
  39. package/dist/src/init/compliance/types.d.ts +30 -27
  40. package/dist/src/init/compliance/types.d.ts.map +1 -1
  41. package/dist/src/init/repo/types.d.ts +11 -34
  42. package/dist/src/init/repo/types.d.ts.map +1 -1
  43. package/dist/src/init/research/src/config/types.d.ts +15 -82
  44. package/dist/src/init/research/src/config/types.d.ts.map +1 -1
  45. package/dist/src/init/research/types.d.ts +38 -93
  46. package/dist/src/init/research/types.d.ts.map +1 -1
  47. package/dist/src/init/team/types.d.ts +4 -42
  48. package/dist/src/init/team/types.d.ts.map +1 -1
  49. package/dist/src/living-docs/smart-doc-organizer.js +1 -1
  50. package/dist/src/living-docs/smart-doc-organizer.js.map +1 -1
  51. package/dist/src/sync/closure-metrics.d.ts +102 -0
  52. package/dist/src/sync/closure-metrics.d.ts.map +1 -0
  53. package/dist/src/sync/closure-metrics.js +267 -0
  54. package/dist/src/sync/closure-metrics.js.map +1 -0
  55. package/dist/src/sync/sync-coordinator.d.ts +29 -0
  56. package/dist/src/sync/sync-coordinator.d.ts.map +1 -1
  57. package/dist/src/sync/sync-coordinator.js +153 -16
  58. package/dist/src/sync/sync-coordinator.js.map +1 -1
  59. package/dist/src/utils/docs-preview/config-generator.d.ts.map +1 -1
  60. package/dist/src/utils/docs-preview/config-generator.js +4 -0
  61. package/dist/src/utils/docs-preview/config-generator.js.map +1 -1
  62. package/dist/src/utils/notification-constants.d.ts +87 -0
  63. package/dist/src/utils/notification-constants.d.ts.map +1 -0
  64. package/dist/src/utils/notification-constants.js +131 -0
  65. package/dist/src/utils/notification-constants.js.map +1 -0
  66. package/dist/src/utils/notification-manager.d.ts +24 -0
  67. package/dist/src/utils/notification-manager.d.ts.map +1 -1
  68. package/dist/src/utils/notification-manager.js +29 -0
  69. package/dist/src/utils/notification-manager.js.map +1 -1
  70. package/dist/src/utils/platform-utils.d.ts +13 -3
  71. package/dist/src/utils/platform-utils.d.ts.map +1 -1
  72. package/dist/src/utils/platform-utils.js +17 -6
  73. package/dist/src/utils/platform-utils.js.map +1 -1
  74. package/package.json +1 -1
  75. package/plugins/specweave/commands/specweave-increment.md +46 -0
  76. package/plugins/specweave/commands/specweave-jobs.md +153 -8
  77. package/plugins/specweave/commands/specweave-judge-llm.md +296 -0
  78. package/plugins/specweave/commands/specweave-organize-docs.md +2 -2
  79. package/plugins/specweave/hooks/hooks.json +10 -0
  80. package/plugins/specweave/hooks/spec-project-validator.sh +24 -2
  81. package/plugins/specweave/hooks/universal/hook-wrapper.cmd +26 -26
  82. package/plugins/specweave/hooks/universal/session-start.cmd +16 -16
  83. package/plugins/specweave/hooks/universal/session-start.ps1 +16 -16
  84. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +87 -0
  85. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.test.sh +302 -0
  86. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +72 -18
  87. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +406 -0
  88. package/plugins/specweave/scripts/session-watchdog.sh +288 -134
  89. package/plugins/specweave/skills/increment-planner/SKILL.md +48 -18
  90. package/plugins/specweave/skills/increment-planner/templates/spec-multi-project.md +27 -14
  91. package/plugins/specweave/skills/increment-planner/templates/spec-single-project.md +16 -5
  92. package/plugins/specweave/skills/spec-generator/SKILL.md +74 -15
  93. package/plugins/specweave-docs/commands/build.md +4 -4
  94. package/plugins/specweave-docs/commands/generate.md +1 -1
  95. package/plugins/specweave-docs/commands/health.md +1 -1
  96. package/plugins/specweave-docs/commands/init.md +1 -1
  97. package/plugins/specweave-docs/commands/organize.md +2 -2
  98. package/plugins/specweave-docs/commands/validate.md +1 -1
  99. package/plugins/specweave-docs/commands/view.md +391 -0
  100. package/plugins/specweave-docs/skills/preview/SKILL.md +56 -17
  101. package/src/templates/AGENTS.md.template +24 -28
  102. package/src/templates/CLAUDE.md.template +12 -8
  103. package/plugins/specweave/commands/specweave-judge.md +0 -276
  104. package/plugins/specweave-docs/commands/preview.md +0 -274
  105. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +0 -738
  106. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +0 -1107
@@ -1,7 +1,14 @@
1
1
  #!/usr/bin/env bash
2
- # SpecWeave Session Watchdog
3
- # Monitors Claude Code sessions and alerts when stuck
4
- # Usage: bash session-watchdog.sh [--daemon] [--interval=60]
2
+ # SpecWeave Session Watchdog v2.0
3
+ # Monitors Claude Code sessions and alerts ONLY when something is REALLY wrong
4
+ #
5
+ # FIXES (2025-12-10):
6
+ # - ELIMINATED FALSE POSITIVES: No longer triggers on stale files alone
7
+ # - SMART DETECTION: Verifies actual stuck processes, not just file ages
8
+ # - SEVERITY LEVELS: Only CRITICAL issues trigger notifications
9
+ # - DIAGNOSTICS: Writes detailed logs for /specweave:jobs to display
10
+ #
11
+ # Usage: bash session-watchdog.sh [--daemon] [--interval=60] [--threshold=300]
5
12
 
6
13
  set -euo pipefail
7
14
 
@@ -10,16 +17,24 @@ STUCK_THRESHOLD_SECONDS="${STUCK_THRESHOLD:-300}" # 5 minutes
10
17
  CHECK_INTERVAL="${CHECK_INTERVAL:-60}" # 1 minute
11
18
  SPECWEAVE_ROOT="${SPECWEAVE_ROOT:-.specweave}"
12
19
  SIGNAL_FILE="${SPECWEAVE_ROOT}/state/.session-stuck"
13
- HEARTBEAT_FILE="${SPECWEAVE_ROOT}/state/.heartbeat"
20
+ DIAGNOSTICS_FILE="${SPECWEAVE_ROOT}/state/.watchdog-diagnostics.json"
14
21
  DAEMON_MODE=false
15
- PROJECT_ROOT="${PWD}"
16
- SESSION_ID="watchdog-$$-$(date +%s)"
17
- COORDINATION_THRESHOLD=30 # Heartbeat threshold for active watchdog detection
22
+ QUIET_MODE=false
23
+
24
+ # Severity levels (only CRITICAL triggers notifications)
25
+ SEVERITY_INFO=0
26
+ SEVERITY_WARNING=1
27
+ SEVERITY_CRITICAL=2
28
+
29
+ # Track consecutive warnings (avoids single-check false positives)
30
+ CONSECUTIVE_WARNINGS=0
31
+ CONSECUTIVE_THRESHOLD=3 # Need 3 consecutive warnings before alerting
18
32
 
19
33
  # Colors
20
34
  RED='\033[0;31m'
21
35
  YELLOW='\033[1;33m'
22
36
  GREEN='\033[0;32m'
37
+ BLUE='\033[0;34m'
23
38
  NC='\033[0m'
24
39
 
25
40
  # Parse arguments
@@ -28,6 +43,9 @@ for arg in "$@"; do
28
43
  --daemon)
29
44
  DAEMON_MODE=true
30
45
  ;;
46
+ --quiet)
47
+ QUIET_MODE=true
48
+ ;;
31
49
  --interval=*)
32
50
  CHECK_INTERVAL="${arg#*=}"
33
51
  ;;
@@ -38,130 +56,316 @@ for arg in "$@"; do
38
56
  done
39
57
 
40
58
  log() {
59
+ [[ "$QUIET_MODE" == "true" ]] && return
41
60
  echo -e "[$(date '+%H:%M:%S')] $1"
42
61
  }
43
62
 
63
+ log_debug() {
64
+ # Always write to diagnostics file even in quiet mode
65
+ echo "[$(date '+%H:%M:%S')] $1" >> "${SPECWEAVE_ROOT}/logs/watchdog.log" 2>/dev/null || true
66
+ }
67
+
68
+ # Send notification ONLY for critical issues
44
69
  send_notification() {
45
- local title="$1"
46
- local message="$2"
70
+ local severity="$1"
71
+ local title="$2"
72
+ local message="$3"
47
73
 
48
- # macOS notification
74
+ # Only notify for CRITICAL severity
75
+ if [[ "$severity" -lt "$SEVERITY_CRITICAL" ]]; then
76
+ log_debug "Skipping notification (severity=$severity, need=$SEVERITY_CRITICAL): $message"
77
+ return
78
+ fi
79
+
80
+ # macOS notification - use "Submarine" for warnings (not alarming like "Basso")
81
+ # Per CLAUDE.md section 11: Basso = ONLY critical errors requiring IMMEDIATE action
82
+ # Zombie detection is recoverable - user can run cleanup script when convenient
49
83
  if command -v osascript &> /dev/null; then
50
- osascript -e "display notification \"$message\" with title \"$title\" sound name \"Basso\""
84
+ osascript -e "display notification \"$message\" with title \"$title\" sound name \"Submarine\""
51
85
  fi
52
86
 
53
- # Linux notification (if available)
87
+ # Linux notification (if available) - use "normal" urgency (not "critical")
88
+ # Critical urgency can bypass Do Not Disturb, which is too aggressive
54
89
  if command -v notify-send &> /dev/null; then
55
- notify-send "$title" "$message" --urgency=critical
90
+ notify-send "$title" "$message" --urgency=normal
56
91
  fi
92
+
93
+ # Windows: notifications sent via PowerShell toast don't have urgency levels
94
+ # They're always non-alarming by design
57
95
  }
58
96
 
59
97
  get_file_age_seconds() {
60
98
  local file="$1"
61
99
  if [[ ! -f "$file" ]]; then
62
- echo "999999"
100
+ echo "-1" # -1 means file doesn't exist (not an error)
63
101
  return
64
102
  fi
65
103
 
66
- local now
67
- local mtime
104
+ local now mtime
68
105
  now=$(date +%s)
69
106
 
70
107
  if [[ "$(uname)" == "Darwin" ]]; then
71
- mtime=$(stat -f %m "$file")
108
+ mtime=$(stat -f %m "$file" 2>/dev/null || echo "$now")
72
109
  else
73
- mtime=$(stat -c %Y "$file")
110
+ mtime=$(stat -c %Y "$file" 2>/dev/null || echo "$now")
74
111
  fi
75
112
 
76
113
  echo $((now - mtime))
77
114
  }
78
115
 
116
+ # Check if a process is actually running
117
+ is_process_running() {
118
+ local pid="$1"
119
+ if [[ -z "$pid" ]] || [[ "$pid" == "0" ]]; then
120
+ return 1
121
+ fi
122
+ kill -0 "$pid" 2>/dev/null
123
+ }
124
+
125
+ # SMART lock file check - verifies the PROCESS is actually stuck, not just file age
79
126
  check_lock_file() {
127
+ local lock_dir="${SPECWEAVE_ROOT}/state/.processor.lock.d"
80
128
  local lock_file="${SPECWEAVE_ROOT}/state/.processor.lock"
81
- if [[ -f "$lock_file" ]]; then
129
+ local result_severity=$SEVERITY_INFO
130
+ local result_message=""
131
+
132
+ # Check new lock directory format first (v2)
133
+ if [[ -d "$lock_dir" ]] && [[ -f "$lock_dir/pid" ]]; then
134
+ local lock_pid
135
+ lock_pid=$(cat "$lock_dir/pid" 2>/dev/null || echo "")
136
+
137
+ if [[ -n "$lock_pid" ]]; then
138
+ if is_process_running "$lock_pid"; then
139
+ # Process is actually running - check how long
140
+ local age
141
+ age=$(get_file_age_seconds "$lock_dir/pid")
142
+ if [[ "$age" -gt "$STUCK_THRESHOLD_SECONDS" ]]; then
143
+ result_severity=$SEVERITY_WARNING
144
+ result_message="Processor PID $lock_pid running for ${age}s (might be legitimate long operation)"
145
+ else
146
+ result_message="Processor PID $lock_pid active (${age}s)"
147
+ fi
148
+ else
149
+ # PID file exists but process is dead = STALE LOCK (cleanup needed, not stuck)
150
+ result_severity=$SEVERITY_WARNING
151
+ result_message="Stale lock: PID $lock_pid no longer running (auto-cleanup will handle)"
152
+ # Don't trigger alert - processor will clean this up on next run
153
+ fi
154
+ fi
155
+ # Check old lock file format (legacy)
156
+ elif [[ -f "$lock_file" ]]; then
82
157
  local age
83
158
  age=$(get_file_age_seconds "$lock_file")
84
159
  if [[ "$age" -gt "$STUCK_THRESHOLD_SECONDS" ]]; then
85
- log "${RED}⚠️ STUCK DETECTED: Lock file held for ${age}s (threshold: ${STUCK_THRESHOLD_SECONDS}s)${NC}"
86
- return 1
160
+ result_severity=$SEVERITY_WARNING
161
+ result_message="Legacy lock file age: ${age}s (consider running cleanup-state.sh)"
87
162
  fi
88
163
  fi
89
- return 0
164
+
165
+ # Write diagnostic
166
+ echo "lock_status=$result_severity" >> "$DIAGNOSTICS_FILE.tmp"
167
+ echo "lock_message=$result_message" >> "$DIAGNOSTICS_FILE.tmp"
168
+
169
+ if [[ -n "$result_message" ]]; then
170
+ log_debug "Lock check: $result_message"
171
+ fi
172
+
173
+ return $result_severity
90
174
  }
91
175
 
92
- check_heartbeat() {
93
- if [[ -f "$HEARTBEAT_FILE" ]]; then
94
- local age
95
- age=$(get_file_age_seconds "$HEARTBEAT_FILE")
96
- if [[ "$age" -gt "$STUCK_THRESHOLD_SECONDS" ]]; then
97
- log "${RED}⚠️ STUCK DETECTED: No heartbeat for ${age}s${NC}"
98
- return 1
99
- fi
176
+ # Check for ACTUAL zombie heredoc processes (CRITICAL - this is a real stuck indicator)
177
+ check_zombie_processes() {
178
+ local result_severity=$SEVERITY_INFO
179
+ local result_message=""
180
+
181
+ # Look for cat processes waiting for EOF (heredoc stuck)
182
+ local cat_zombies
183
+ cat_zombies=$(pgrep -f "cat.*EOF" 2>/dev/null | wc -l | tr -d ' \n' || echo "0")
184
+ cat_zombies="${cat_zombies:-0}"
185
+ [[ ! "$cat_zombies" =~ ^[0-9]+$ ]] && cat_zombies=0
186
+
187
+ # Look for bash processes that seem stuck on heredoc
188
+ local bash_heredocs
189
+ bash_heredocs=$(pgrep -f "bash.*<<" 2>/dev/null | wc -l | tr -d ' \n' || echo "0")
190
+ bash_heredocs="${bash_heredocs:-0}"
191
+ [[ ! "$bash_heredocs" =~ ^[0-9]+$ ]] && bash_heredocs=0
192
+
193
+ local total_zombies=$((cat_zombies + bash_heredocs))
194
+
195
+ if [[ "$total_zombies" -gt 0 ]]; then
196
+ result_severity=$SEVERITY_CRITICAL # This is DEFINITELY stuck!
197
+ result_message="$total_zombies zombie heredoc processes detected (cat=$cat_zombies, bash=$bash_heredocs)"
198
+ log "${RED}🚨 CRITICAL: $result_message${NC}"
100
199
  fi
101
- return 0
200
+
201
+ echo "zombie_count=$total_zombies" >> "$DIAGNOSTICS_FILE.tmp"
202
+ echo "zombie_message=$result_message" >> "$DIAGNOSTICS_FILE.tmp"
203
+
204
+ return $result_severity
102
205
  }
103
206
 
104
- check_mcp_drops() {
207
+ # Check MCP connection health (WARNING level, not critical)
208
+ check_mcp_health() {
209
+ local result_severity=$SEVERITY_INFO
210
+ local result_message=""
211
+ local drops=0
105
212
  local debug_log="$HOME/.claude/debug/latest"
213
+
106
214
  if [[ -f "$debug_log" ]]; then
107
- local drops
108
- drops=$(grep -c "WS-IDE connection dropped" "$debug_log" 2>/dev/null | head -1 || echo "0")
215
+ # Count MCP drops in last 500 lines
216
+ drops=$(tail -500 "$debug_log" 2>/dev/null | grep -c "WS-IDE connection dropped" 2>/dev/null || echo "0")
109
217
  drops="${drops//[^0-9]/}"
110
218
  drops="${drops:-0}"
111
- if [[ "$drops" -gt 3 ]]; then
112
- log "${YELLOW}⚠️ MCP instability: $drops connection drops detected${NC}"
113
- return 1
219
+ [[ ! "$drops" =~ ^[0-9]+$ ]] && drops=0
220
+
221
+ if [[ "$drops" -gt 10 ]]; then
222
+ result_severity=$SEVERITY_WARNING
223
+ result_message="MCP instability: $drops connection drops (consider restarting VS Code Extension Host)"
224
+ elif [[ "$drops" -gt 3 ]]; then
225
+ result_message="MCP: $drops drops detected (minor instability)"
114
226
  fi
115
227
  fi
116
- return 0
117
- }
118
228
 
119
- check_zombie_processes() {
120
- local zombies
121
- zombies=$(pgrep -f "cat.*EOF" 2>/dev/null | wc -l | tr -d ' ')
122
- if [[ "$zombies" -gt 0 ]]; then
123
- log "${RED}⚠️ STUCK DETECTED: $zombies zombie heredoc processes${NC}"
124
- return 1
125
- fi
126
- return 0
127
- }
229
+ echo "mcp_drops=$drops" >> "$DIAGNOSTICS_FILE.tmp"
230
+ echo "mcp_message=$result_message" >> "$DIAGNOSTICS_FILE.tmp"
128
231
 
129
- check_session_health() {
130
- local stuck=false
131
- local reasons=()
232
+ return $result_severity
233
+ }
132
234
 
133
- if ! check_lock_file; then
134
- stuck=true
135
- reasons+=("Lock file stale")
235
+ # Check for orphaned background jobs (informational, not critical)
236
+ check_orphaned_jobs() {
237
+ local result_severity=$SEVERITY_INFO
238
+ local result_message=""
239
+ local jobs_dir="${SPECWEAVE_ROOT}/state/jobs"
240
+ local orphaned_count=0
241
+
242
+ if [[ -d "$jobs_dir" ]]; then
243
+ for job_dir in "$jobs_dir"/*/; do
244
+ [[ ! -d "$job_dir" ]] && continue
245
+
246
+ local pid_file="${job_dir}worker.pid"
247
+ local config_file="${job_dir}config.json"
248
+
249
+ if [[ -f "$pid_file" ]] && [[ -f "$config_file" ]]; then
250
+ local pid
251
+ pid=$(cat "$pid_file" 2>/dev/null || echo "")
252
+ if [[ -n "$pid" ]] && ! is_process_running "$pid"; then
253
+ orphaned_count=$((orphaned_count + 1))
254
+ fi
255
+ fi
256
+ done
136
257
  fi
137
258
 
138
- if ! check_heartbeat; then
139
- stuck=true
140
- reasons+=("No heartbeat")
259
+ if [[ "$orphaned_count" -gt 0 ]]; then
260
+ result_message="$orphaned_count orphaned job(s) found (run /specweave:jobs to see details)"
141
261
  fi
142
262
 
143
- if ! check_mcp_drops; then
144
- reasons+=("MCP unstable")
145
- fi
263
+ echo "orphaned_jobs=$orphaned_count" >> "$DIAGNOSTICS_FILE.tmp"
264
+ echo "orphaned_message=$result_message" >> "$DIAGNOSTICS_FILE.tmp"
265
+
266
+ return $result_severity
267
+ }
268
+
269
+ # Write diagnostics file for /specweave:jobs to read
270
+ write_diagnostics() {
271
+ local overall_severity="$1"
272
+ local overall_status="$2"
273
+
274
+ {
275
+ echo "{"
276
+ echo " \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
277
+ echo " \"severity\": $overall_severity,"
278
+ echo " \"status\": \"$overall_status\","
279
+
280
+ # Parse temp file into JSON
281
+ local lock_status="" lock_message="" zombie_count="" zombie_message=""
282
+ local mcp_drops="" mcp_message="" orphaned_jobs="" orphaned_message=""
283
+
284
+ while IFS='=' read -r key value; do
285
+ case "$key" in
286
+ lock_status) lock_status="$value" ;;
287
+ lock_message) lock_message="$value" ;;
288
+ zombie_count) zombie_count="$value" ;;
289
+ zombie_message) zombie_message="$value" ;;
290
+ mcp_drops) mcp_drops="$value" ;;
291
+ mcp_message) mcp_message="$value" ;;
292
+ orphaned_jobs) orphaned_jobs="$value" ;;
293
+ orphaned_message) orphaned_message="$value" ;;
294
+ esac
295
+ done < "$DIAGNOSTICS_FILE.tmp" 2>/dev/null || true
296
+
297
+ # Sanitize numeric values (ensure single digit format)
298
+ lock_status="${lock_status:-0}"; [[ ! "$lock_status" =~ ^[0-9]+$ ]] && lock_status=0
299
+ zombie_count="${zombie_count:-0}"; [[ ! "$zombie_count" =~ ^[0-9]+$ ]] && zombie_count=0
300
+ mcp_drops="${mcp_drops:-0}"; [[ ! "$mcp_drops" =~ ^[0-9]+$ ]] && mcp_drops=0
301
+ orphaned_jobs="${orphaned_jobs:-0}"; [[ ! "$orphaned_jobs" =~ ^[0-9]+$ ]] && orphaned_jobs=0
302
+
303
+ echo " \"checks\": {"
304
+ echo " \"lock\": { \"severity\": $((lock_status)), \"message\": \"${lock_message:-ok}\" },"
305
+ echo " \"zombies\": { \"count\": $((zombie_count)), \"message\": \"${zombie_message:-none}\" },"
306
+ echo " \"mcp\": { \"drops\": $((mcp_drops)), \"message\": \"${mcp_message:-stable}\" },"
307
+ echo " \"orphanedJobs\": { \"count\": $((orphaned_jobs)), \"message\": \"${orphaned_message:-none}\" }"
308
+ echo " },"
309
+ echo " \"consecutiveWarnings\": $CONSECUTIVE_WARNINGS,"
310
+ echo " \"thresholdSeconds\": $STUCK_THRESHOLD_SECONDS,"
311
+ echo " \"checkIntervalSeconds\": $CHECK_INTERVAL"
312
+ echo "}"
313
+ } > "$DIAGNOSTICS_FILE"
314
+
315
+ rm -f "$DIAGNOSTICS_FILE.tmp"
316
+ }
146
317
 
147
- if ! check_zombie_processes; then
148
- stuck=true
149
- reasons+=("Zombie processes")
318
+ check_session_health() {
319
+ local max_severity=$SEVERITY_INFO
320
+ local issues=()
321
+
322
+ # Initialize temp diagnostics file
323
+ mkdir -p "$(dirname "$DIAGNOSTICS_FILE")"
324
+ : > "$DIAGNOSTICS_FILE.tmp"
325
+
326
+ # Run all checks
327
+ check_lock_file || true
328
+ local lock_sev=$?
329
+ [[ $lock_sev -gt $max_severity ]] && max_severity=$lock_sev
330
+
331
+ check_zombie_processes || true
332
+ local zombie_sev=$?
333
+ [[ $zombie_sev -gt $max_severity ]] && max_severity=$zombie_sev
334
+
335
+ check_mcp_health || true
336
+ local mcp_sev=$?
337
+ [[ $mcp_sev -gt $max_severity ]] && max_severity=$mcp_sev
338
+
339
+ check_orphaned_jobs || true
340
+
341
+ # Determine overall status
342
+ local overall_status="healthy"
343
+ if [[ $max_severity -eq $SEVERITY_CRITICAL ]]; then
344
+ overall_status="critical"
345
+ CONSECUTIVE_WARNINGS=$((CONSECUTIVE_WARNINGS + 1))
346
+ elif [[ $max_severity -eq $SEVERITY_WARNING ]]; then
347
+ overall_status="warning"
348
+ CONSECUTIVE_WARNINGS=$((CONSECUTIVE_WARNINGS + 1))
349
+ else
350
+ overall_status="healthy"
351
+ CONSECUTIVE_WARNINGS=0 # Reset on healthy check
150
352
  fi
151
353
 
152
- if [[ "$stuck" == "true" ]]; then
153
- local reason_str
154
- reason_str=$(IFS=", "; echo "${reasons[*]}")
354
+ # Write diagnostics for /specweave:jobs
355
+ write_diagnostics "$max_severity" "$overall_status"
155
356
 
357
+ # Only alert if CRITICAL and seen multiple consecutive times
358
+ if [[ $max_severity -eq $SEVERITY_CRITICAL ]] && [[ $CONSECUTIVE_WARNINGS -ge $CONSECUTIVE_THRESHOLD ]]; then
156
359
  # Create signal file
157
360
  echo "stuck_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$SIGNAL_FILE"
158
- echo "reasons=$reason_str" >> "$SIGNAL_FILE"
361
+ echo "severity=critical" >> "$SIGNAL_FILE"
362
+ echo "consecutive_warnings=$CONSECUTIVE_WARNINGS" >> "$SIGNAL_FILE"
159
363
 
160
- send_notification "🚨 Claude Code Stuck" "$reason_str - Run cleanup-state.sh"
364
+ send_notification $SEVERITY_CRITICAL "🚨 Claude Code STUCK" "Zombie processes detected - Run cleanup-state.sh"
161
365
 
162
366
  log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
163
- log "${RED}SESSION STUCK DETECTED${NC}"
164
- log "${RED}Reasons: $reason_str${NC}"
367
+ log "${RED}CRITICAL: SESSION STUCK DETECTED${NC}"
368
+ log "${RED}Consecutive warnings: $CONSECUTIVE_WARNINGS${NC}"
165
369
  log ""
166
370
  log "Recovery steps:"
167
371
  log " 1. Press Ctrl+C multiple times in Claude Code terminal"
@@ -171,85 +375,35 @@ check_session_health() {
171
375
  log "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
172
376
 
173
377
  return 1
378
+
379
+ elif [[ $max_severity -eq $SEVERITY_WARNING ]]; then
380
+ # Log warning but don't notify (might be false positive)
381
+ log "${YELLOW}⚠️ Warning detected (${CONSECUTIVE_WARNINGS}/${CONSECUTIVE_THRESHOLD} before alert)${NC}"
382
+ return 0
383
+
174
384
  else
175
- # Remove signal file if exists
385
+ # Healthy - remove signal file if exists
176
386
  rm -f "$SIGNAL_FILE"
177
387
  log "${GREEN}✓ Session healthy${NC}"
178
388
  return 0
179
389
  fi
180
390
  }
181
391
 
182
- # Coordination Functions
183
- check_active_watchdog() {
184
- # Check if another watchdog is running via session registry
185
- node "${PROJECT_ROOT}/dist/src/cli/check-watchdog.js" 2>/dev/null || echo ""
186
- }
187
-
188
- register_watchdog() {
189
- # Register this watchdog in session registry
190
- node "${PROJECT_ROOT}/dist/src/cli/register-session.js" "$SESSION_ID" $$ "watchdog" 2>&1 | \
191
- grep -v "^$" | head -3
192
- }
193
-
194
- update_watchdog_heartbeat() {
195
- # Update watchdog heartbeat
196
- node "${PROJECT_ROOT}/dist/src/cli/update-heartbeat.js" "$SESSION_ID" 2>/dev/null || true
197
- }
198
-
199
- cleanup_watchdog() {
200
- # Remove watchdog from registry on exit
201
- node "${PROJECT_ROOT}/dist/src/cli/remove-session.js" "$SESSION_ID" 2>&1 | \
202
- grep -v "^$" | head -3
203
- log "Watchdog cleanup complete"
204
- }
205
-
206
- run_cleanup_service() {
207
- # Run zombie process cleanup
208
- node "${PROJECT_ROOT}/dist/src/cli/cleanup-zombies.js" 60 2>&1 | \
209
- grep -v "^$" | head -10 || true
210
- }
211
-
212
392
  # Main execution
213
393
  if [[ "$DAEMON_MODE" == "true" ]]; then
214
- # Coordination check before starting daemon
215
- active_watchdog=$(check_active_watchdog)
216
-
217
- if [[ -n "$active_watchdog" ]]; then
218
- log "${YELLOW}Watchdog already active (PID: $active_watchdog)${NC}"
219
- log "Exiting to avoid duplicate watchdogs"
220
- exit 0
221
- fi
222
-
223
- # Register as watchdog
224
- register_watchdog
225
-
226
- # Trap signals for graceful shutdown
227
- trap cleanup_watchdog SIGTERM SIGINT EXIT
228
-
229
- log "Starting session watchdog daemon (interval: ${CHECK_INTERVAL}s, threshold: ${STUCK_THRESHOLD_SECONDS}s)"
230
- log "Watchdog session: $SESSION_ID (PID: $$)"
394
+ log "${BLUE}Starting session watchdog v2.0 (smart detection, CRITICAL-only alerts)${NC}"
395
+ log " Interval: ${CHECK_INTERVAL}s | Threshold: ${STUCK_THRESHOLD_SECONDS}s"
396
+ log " Consecutive warnings needed: ${CONSECUTIVE_THRESHOLD}"
397
+ log " Diagnostics: ${DIAGNOSTICS_FILE}"
231
398
  log "Press Ctrl+C to stop"
232
399
 
233
- while true; do
234
- # Update own heartbeat
235
- update_watchdog_heartbeat
400
+ # Create log directory
401
+ mkdir -p "${SPECWEAVE_ROOT}/logs"
236
402
 
237
- # Check session health
403
+ while true; do
238
404
  check_session_health || true
239
-
240
- # Run cleanup service
241
- run_cleanup_service
242
-
243
- # Check if parent process still exists (if we have a parent session)
244
- if ! kill -0 $PPID 2>/dev/null; then
245
- log "${YELLOW}Parent process died, exiting watchdog${NC}"
246
- break
247
- fi
248
-
249
405
  sleep "$CHECK_INTERVAL"
250
406
  done
251
-
252
- cleanup_watchdog
253
407
  else
254
408
  log "Running single health check..."
255
409
  check_session_health
@@ -178,26 +178,29 @@ echo "Using coverageTarget: $coverageTarget"
178
178
 
179
179
  ### STEP 0B: Get Project Context (MANDATORY - BLOCKING!)
180
180
 
181
- **⛔ DO NOT PROCEED TO STEP 1 WITHOUT COMPLETING THIS STEP!**
181
+ **⛔ THIS IS A HARD BLOCK - YOU CANNOT PROCEED WITHOUT PROJECT CONTEXT!**
182
182
 
183
- Before generating ANY spec.md content, you MUST run this CLI command:
183
+ **🚨 FAILURE TO COMPLETE THIS STEP = spec.md WILL BE BLOCKED BY VALIDATION HOOK!**
184
184
 
185
+ Before generating ANY spec.md content, you MUST:
186
+
187
+ **1. RUN THE CONTEXT API (via Bash tool):**
185
188
  ```bash
186
189
  specweave context projects
187
190
  ```
188
191
 
189
- This returns JSON with available projects and structure level:
192
+ **2. CAPTURE AND STORE THE OUTPUT:**
190
193
 
194
+ For 1-level structures:
191
195
  ```json
192
196
  {
193
197
  "level": 1,
194
198
  "projects": [{"id": "my-app", "name": "My App"}],
195
- "detectionReason": "multiProject configuration",
196
- "source": "multi-project"
199
+ "detectionReason": "multiProject configuration"
197
200
  }
198
201
  ```
199
202
 
200
- **For 2-level structures**, output includes boards:
203
+ For 2-level structures (ADO/JIRA boards):
201
204
  ```json
202
205
  {
203
206
  "level": 2,
@@ -207,27 +210,54 @@ This returns JSON with available projects and structure level:
207
210
  {"id": "digital-ops", "name": "Digital Operations"},
208
211
  {"id": "mobile-team", "name": "Mobile Team"}
209
212
  ]
210
- },
211
- "detectionReason": "ADO area path mapping configured",
212
- "source": "ado-area-path"
213
+ }
213
214
  }
214
215
  ```
215
216
 
216
- **VALIDATION RULES:**
217
+ **3. RESOLVE PROJECT/BOARD FOR EACH USER STORY:**
218
+
219
+ ```
220
+ CONTEXT_OUTPUT = <output from specweave context projects>
221
+
222
+ For each US you will generate:
223
+ IF CONTEXT_OUTPUT.level == 1:
224
+ US.project = select from CONTEXT_OUTPUT.projects[].id
225
+
226
+ IF CONTEXT_OUTPUT.level == 2:
227
+ US.project = select from CONTEXT_OUTPUT.projects[].id
228
+ US.board = select from CONTEXT_OUTPUT.boardsByProject[project][].id
229
+ ```
230
+
231
+ **4. NOW PROCEED TO STEP 1 (with resolved values stored)**
232
+
233
+ ---
234
+
235
+ **VALIDATION RULES (ENFORCED BY HOOK):**
217
236
 
218
237
  ```
219
- ✅ REQUIRED: Parse the JSON output and use ONLY those project/board values
220
- ✅ REQUIRED: project field MUST match one of the returned projects[].id
221
- ✅ REQUIRED: board field (2-level) MUST match one of boardsByProject[project].id
222
- FORBIDDEN: Inventing or guessing project names
223
- FORBIDDEN: Using folder name as project (e.g., "sw-olysense")
224
- ❌ FORBIDDEN: Creating spec.md with {{PROJECT_ID}} placeholder
238
+ ✅ REQUIRED: Actually RUN "specweave context projects" command
239
+ ✅ REQUIRED: Parse the JSON and extract project IDs
240
+ ✅ REQUIRED: project field MUST match one of projects[].id from output
241
+ REQUIRED: board field (2-level) MUST match one of boardsByProject[project][].id
242
+ REQUIRED: Each US has **Project**: and **Board**: (2-level) with RESOLVED values
243
+
244
+ ❌ FORBIDDEN: Skipping this step and generating spec.md directly
245
+ ❌ FORBIDDEN: Inventing project names not in the API output
246
+ ❌ FORBIDDEN: Using folder names as project (e.g., "my-project-folder")
247
+ ❌ FORBIDDEN: Using {{PROJECT_ID}} or {{BOARD_ID}} placeholders
225
248
  ❌ FORBIDDEN: Creating spec.md for 2-level without board: field
249
+ ❌ FORBIDDEN: Generating spec.md without running context API first
226
250
  ```
227
251
 
252
+ **WHY THIS IS BLOCKING:**
253
+ - Hook `spec-project-validator.sh` BLOCKS spec.md with placeholders or invalid projects
254
+ - Without resolved project/board, living docs sync FAILS
255
+ - Without resolved project/board, external tool sync (GitHub/JIRA/ADO) FAILS
256
+ - User gets blocked error and must manually fix - BAD UX!
257
+
228
258
  **Structure Levels:**
229
- - **1-Level**: `internal/specs/{project}/FS-XXX/` - requires `project` in spec.md
230
- - **2-Level**: `internal/specs/{project}/{board}/FS-XXX/` - requires BOTH `project` AND `board`
259
+ - **1-Level**: `internal/specs/{project}/FS-XXX/` - requires `project` per US
260
+ - **2-Level**: `internal/specs/{project}/{board}/FS-XXX/` - requires `project` AND `board` per US
231
261
 
232
262
  **Alternative: Interactive Selection:**
233
263
  ```bash