specweave 1.0.28 → 1.0.30

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 (34) hide show
  1. package/CLAUDE.md +39 -0
  2. package/package.json +1 -1
  3. package/plugins/specweave/.claude-plugin/plugin.json +1 -1
  4. package/plugins/specweave/commands/check-hooks.md +43 -0
  5. package/plugins/specweave/hooks/lib/circuit-breaker.sh +381 -0
  6. package/plugins/specweave/hooks/lib/logging.sh +231 -0
  7. package/plugins/specweave/hooks/lib/metrics.sh +347 -0
  8. package/plugins/specweave/hooks/lib/semaphore.sh +216 -0
  9. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +156 -22
  10. package/plugins/specweave/scripts/hook-health.sh +441 -0
  11. package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
  12. package/plugins/specweave-alternatives/.claude-plugin/plugin.json +1 -1
  13. package/plugins/specweave-backend/.claude-plugin/plugin.json +1 -1
  14. package/plugins/specweave-confluent/.claude-plugin/plugin.json +1 -1
  15. package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +1 -1
  16. package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
  17. package/plugins/specweave-docs/.claude-plugin/plugin.json +1 -1
  18. package/plugins/specweave-figma/.claude-plugin/plugin.json +1 -1
  19. package/plugins/specweave-frontend/.claude-plugin/plugin.json +1 -1
  20. package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
  21. package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
  22. package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
  23. package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
  24. package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
  25. package/plugins/specweave-kubernetes/.claude-plugin/plugin.json +1 -1
  26. package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
  27. package/plugins/specweave-mobile/.claude-plugin/plugin.json +1 -1
  28. package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
  29. package/plugins/specweave-payments/.claude-plugin/plugin.json +1 -1
  30. package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +1 -1
  31. package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
  32. package/plugins/specweave-testing/.claude-plugin/plugin.json +1 -1
  33. package/plugins/specweave-ui/.claude-plugin/plugin.json +1 -1
  34. /package/plugins/specweave/hooks/{hooks.json.bak → hooks.json} +0 -0
@@ -0,0 +1,216 @@
1
+ #!/bin/bash
2
+ # semaphore.sh - File-based semaphore for limiting concurrent hook execution
3
+ #
4
+ # PROBLEM SOLVED:
5
+ # Process storms occur when many hooks spawn simultaneously, overwhelming the system.
6
+ # Instead of detecting storms and blocking EVERYTHING (current behavior), this semaphore
7
+ # limits concurrency properly - excess requests wait or timeout gracefully.
8
+ #
9
+ # USAGE:
10
+ # source semaphore.sh
11
+ # if acquire_semaphore "hook-name" 10 5000; then
12
+ # # Do work
13
+ # release_semaphore "hook-name"
14
+ # else
15
+ # # Timeout - return safe default
16
+ # fi
17
+ #
18
+ # DESIGN:
19
+ # - Uses file-based locks (portable, no dependencies)
20
+ # - Configurable max concurrent slots
21
+ # - Configurable timeout with exponential backoff
22
+ # - Auto-cleanup of stale locks (older than 30s)
23
+ # - Request queuing with FIFO ordering
24
+ #
25
+ # v1.0.0 - Initial implementation (2025-12-17)
26
+
27
+ set -o pipefail
28
+
29
+ # === Configuration ===
30
+ SEMAPHORE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/semaphores"
31
+ SEMAPHORE_MAX_AGE_SECONDS=30 # Stale lock threshold
32
+ SEMAPHORE_DEBUG="${SEMAPHORE_DEBUG:-0}"
33
+
34
+ # === Initialization ===
35
+ _init_semaphore_dir() {
36
+ mkdir -p "$SEMAPHORE_DIR" 2>/dev/null || true
37
+ }
38
+
39
+ # === Logging ===
40
+ _sem_log() {
41
+ [[ "$SEMAPHORE_DEBUG" == "1" ]] && echo "[SEM $(date +%H:%M:%S.%3N)] $*" >&2
42
+ }
43
+
44
+ # === Cleanup stale locks ===
45
+ # Locks older than SEMAPHORE_MAX_AGE_SECONDS are considered abandoned
46
+ cleanup_stale_locks() {
47
+ local name="$1"
48
+ local lock_dir="$SEMAPHORE_DIR/$name"
49
+
50
+ [[ ! -d "$lock_dir" ]] && return 0
51
+
52
+ local now
53
+ now=$(date +%s)
54
+
55
+ for lock_file in "$lock_dir"/*.lock; do
56
+ [[ ! -f "$lock_file" ]] && continue
57
+
58
+ local mtime
59
+ if [[ "$(uname)" == "Darwin" ]]; then
60
+ mtime=$(stat -f %m "$lock_file" 2>/dev/null || echo "0")
61
+ else
62
+ mtime=$(stat -c %Y "$lock_file" 2>/dev/null || echo "0")
63
+ fi
64
+
65
+ local age=$((now - mtime))
66
+ if [[ "$age" -gt "$SEMAPHORE_MAX_AGE_SECONDS" ]]; then
67
+ _sem_log "Cleaning stale lock: $lock_file (age: ${age}s)"
68
+ rm -f "$lock_file" 2>/dev/null || true
69
+ fi
70
+ done
71
+ }
72
+
73
+ # === Count active slots ===
74
+ count_active_slots() {
75
+ local name="$1"
76
+ local lock_dir="$SEMAPHORE_DIR/$name"
77
+
78
+ [[ ! -d "$lock_dir" ]] && echo "0" && return
79
+
80
+ local count=0
81
+ for lock_file in "$lock_dir"/*.lock; do
82
+ [[ -f "$lock_file" ]] && count=$((count + 1))
83
+ done
84
+
85
+ echo "$count"
86
+ }
87
+
88
+ # === Acquire semaphore slot ===
89
+ # Args: name, max_slots, timeout_ms
90
+ # Returns: 0 if acquired, 1 if timeout
91
+ acquire_semaphore() {
92
+ local name="${1:-default}"
93
+ local max_slots="${2:-10}"
94
+ local timeout_ms="${3:-5000}"
95
+
96
+ _init_semaphore_dir
97
+
98
+ local lock_dir="$SEMAPHORE_DIR/$name"
99
+ mkdir -p "$lock_dir" 2>/dev/null || true
100
+
101
+ # Generate unique slot ID (use random instead of %N which doesn't work on macOS)
102
+ local slot_id="$$-$RANDOM$RANDOM"
103
+ local lock_file="$lock_dir/${slot_id}.lock"
104
+
105
+ # Store slot ID for release
106
+ export _SEMAPHORE_SLOT_ID="$slot_id"
107
+ export _SEMAPHORE_NAME="$name"
108
+ export _SEMAPHORE_LOCK_FILE="$lock_file"
109
+
110
+ local start_time
111
+ # macOS doesn't support %N, use seconds * 1000 as approximation
112
+ if command -v gdate &>/dev/null; then
113
+ start_time=$(gdate +%s%3N)
114
+ else
115
+ start_time=$(($(date +%s) * 1000))
116
+ fi
117
+
118
+ local attempt=0
119
+ local backoff_ms=10 # Start with 10ms backoff
120
+ local max_backoff_ms=200
121
+
122
+ while true; do
123
+ # Cleanup stale locks periodically (every 5 attempts)
124
+ [[ $((attempt % 5)) -eq 0 ]] && cleanup_stale_locks "$name"
125
+
126
+ local current_slots
127
+ current_slots=$(count_active_slots "$name")
128
+
129
+ if [[ "$current_slots" -lt "$max_slots" ]]; then
130
+ # Try to acquire slot atomically
131
+ if (set -o noclobber; echo "$$" > "$lock_file") 2>/dev/null; then
132
+ _sem_log "Acquired slot $slot_id for $name (slots: $((current_slots + 1))/$max_slots)"
133
+ return 0
134
+ fi
135
+ fi
136
+
137
+ # Check timeout
138
+ local now
139
+ if command -v gdate &>/dev/null; then
140
+ now=$(gdate +%s%3N)
141
+ else
142
+ now=$(($(date +%s) * 1000))
143
+ fi
144
+ local elapsed=$((now - start_time))
145
+
146
+ if [[ "$elapsed" -ge "$timeout_ms" ]]; then
147
+ _sem_log "Timeout acquiring $name after ${elapsed}ms (slots: $current_slots/$max_slots)"
148
+ return 1
149
+ fi
150
+
151
+ # Exponential backoff with jitter
152
+ local jitter=$((RANDOM % 20))
153
+ local sleep_ms=$((backoff_ms + jitter))
154
+
155
+ _sem_log "Waiting for slot $name (attempt $attempt, backoff ${sleep_ms}ms, slots: $current_slots/$max_slots)"
156
+
157
+ # Sleep (convert ms to fractional seconds)
158
+ sleep "0.$(printf '%03d' $sleep_ms)" 2>/dev/null || sleep 0.1
159
+
160
+ # Increase backoff (exponential with cap)
161
+ backoff_ms=$((backoff_ms * 2))
162
+ [[ "$backoff_ms" -gt "$max_backoff_ms" ]] && backoff_ms=$max_backoff_ms
163
+
164
+ attempt=$((attempt + 1))
165
+ done
166
+ }
167
+
168
+ # === Release semaphore slot ===
169
+ release_semaphore() {
170
+ local name="${1:-$_SEMAPHORE_NAME}"
171
+ local lock_file="${_SEMAPHORE_LOCK_FILE}"
172
+
173
+ if [[ -n "$lock_file" ]] && [[ -f "$lock_file" ]]; then
174
+ rm -f "$lock_file" 2>/dev/null || true
175
+ _sem_log "Released slot for $name"
176
+ fi
177
+
178
+ unset _SEMAPHORE_SLOT_ID
179
+ unset _SEMAPHORE_NAME
180
+ unset _SEMAPHORE_LOCK_FILE
181
+ }
182
+
183
+ # === Get semaphore status ===
184
+ get_semaphore_status() {
185
+ local name="${1:-default}"
186
+ local max_slots="${2:-10}"
187
+
188
+ _init_semaphore_dir
189
+ cleanup_stale_locks "$name"
190
+
191
+ local active
192
+ active=$(count_active_slots "$name")
193
+
194
+ echo "{\"name\":\"$name\",\"active\":$active,\"max\":$max_slots,\"available\":$((max_slots - active))}"
195
+ }
196
+
197
+ # === Force release all slots (emergency) ===
198
+ force_release_all() {
199
+ local name="${1:-default}"
200
+ local lock_dir="$SEMAPHORE_DIR/$name"
201
+
202
+ if [[ -d "$lock_dir" ]]; then
203
+ rm -f "$lock_dir"/*.lock 2>/dev/null || true
204
+ _sem_log "Force released all slots for $name"
205
+ fi
206
+ }
207
+
208
+ # === Trap handler for automatic cleanup ===
209
+ _semaphore_cleanup_trap() {
210
+ release_semaphore 2>/dev/null || true
211
+ }
212
+
213
+ # Register cleanup trap if sourced
214
+ if [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then
215
+ trap _semaphore_cleanup_trap EXIT INT TERM
216
+ fi
@@ -1,37 +1,70 @@
1
1
  #!/bin/bash
2
- # fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks
2
+ # fail-fast-wrapper.sh - HARD TIMEOUT wrapper for all hooks with proper concurrency control
3
3
  # If ANY hook takes longer than HOOK_TIMEOUT, it gets KILLED.
4
4
  #
5
5
  # Usage: bash fail-fast-wrapper.sh <hook-script> [args...]
6
6
  #
7
7
  # Environment:
8
- # HOOK_TIMEOUT - max seconds (default: 5)
9
- # HOOK_DEBUG - set to 1 for verbose logging
8
+ # HOOK_TIMEOUT - max seconds (default: 5)
9
+ # HOOK_DEBUG - set to 1 for verbose logging
10
+ # HOOK_MAX_CONCURRENT - max concurrent hooks (default: 15)
11
+ # HOOK_ACQUIRE_TIMEOUT_MS - semaphore acquire timeout (default: 3000)
10
12
  #
11
13
  # Exit behavior:
12
14
  # - Returns hook output on success
13
15
  # - Returns safe JSON on timeout ({"continue":true} or {"decision":"approve"})
14
16
  # - NEVER hangs - timeout is enforced with SIGKILL
15
17
  #
16
- # CRASH PREVENTION:
17
- # - Integrates with crash-prevention.sh for process storm detection
18
- # - Auto-kills zombie processes on timeout
19
- # - Records failures for circuit breaker
18
+ # CONCURRENCY CONTROL (v1.0.30):
19
+ # - Semaphore-based concurrency limiting (NOT process storm detection)
20
+ # - Proper circuit breaker with CLOSED/OPEN/HALF_OPEN states
21
+ # - Structured logging with request tracing
22
+ # - Metrics collection for observability
20
23
  #
21
24
  # v0.33.0 - Enhanced with crash prevention integration
25
+ # v1.0.30 - Complete rewrite with proper concurrency primitives
22
26
 
23
27
  set -o pipefail
24
28
 
25
29
  # === Configuration ===
26
30
  HOOK_TIMEOUT="${HOOK_TIMEOUT:-5}" # 5 seconds - more than enough for any hook
27
31
  HOOK_DEBUG="${HOOK_DEBUG:-0}"
32
+ HOOK_MAX_CONCURRENT="${HOOK_MAX_CONCURRENT:-15}" # Max concurrent hooks
33
+ HOOK_ACQUIRE_TIMEOUT_MS="${HOOK_ACQUIRE_TIMEOUT_MS:-3000}" # 3 seconds to acquire semaphore
28
34
  LOG_FILE="${HOME}/.claude/hook-failures.log"
29
35
 
30
- # === Crash Prevention Integration ===
36
+ # === Library paths ===
31
37
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32
- CRASH_PREVENTION="${SCRIPT_DIR}/../lib/crash-prevention.sh"
38
+ LIB_DIR="${SCRIPT_DIR}/../lib"
33
39
 
34
- # Source crash prevention if available (non-blocking)
40
+ # === Source libraries (fail gracefully if missing) ===
41
+ SEMAPHORE_LOADED=false
42
+ CIRCUIT_BREAKER_LOADED=false
43
+ LOGGING_LOADED=false
44
+ METRICS_LOADED=false
45
+
46
+ # Set state dir for all libraries
47
+ export SPECWEAVE_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}"
48
+ export SPECWEAVE_LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
49
+
50
+ if [[ -f "$LIB_DIR/semaphore.sh" ]]; then
51
+ source "$LIB_DIR/semaphore.sh" 2>/dev/null && SEMAPHORE_LOADED=true
52
+ fi
53
+
54
+ if [[ -f "$LIB_DIR/circuit-breaker.sh" ]]; then
55
+ source "$LIB_DIR/circuit-breaker.sh" 2>/dev/null && CIRCUIT_BREAKER_LOADED=true
56
+ fi
57
+
58
+ if [[ -f "$LIB_DIR/logging.sh" ]]; then
59
+ source "$LIB_DIR/logging.sh" 2>/dev/null && LOGGING_LOADED=true
60
+ fi
61
+
62
+ if [[ -f "$LIB_DIR/metrics.sh" ]]; then
63
+ source "$LIB_DIR/metrics.sh" 2>/dev/null && METRICS_LOADED=true
64
+ fi
65
+
66
+ # Legacy crash prevention (fallback)
67
+ CRASH_PREVENTION="${LIB_DIR}/crash-prevention.sh"
35
68
  if [[ -f "$CRASH_PREVENTION" ]]; then
36
69
  source "$CRASH_PREVENTION" 2>/dev/null || true
37
70
  fi
@@ -44,7 +77,7 @@ log_debug() {
44
77
  log_failure() {
45
78
  local msg="$1"
46
79
  mkdir -p "$(dirname "$LOG_FILE")"
47
- echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK TIMEOUT: $msg" >> "$LOG_FILE"
80
+ echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK FAILURE: $msg" >> "$LOG_FILE"
48
81
  }
49
82
 
50
83
  # === Safe JSON output based on hook type ===
@@ -58,6 +91,12 @@ get_safe_output() {
58
91
  fi
59
92
  }
60
93
 
94
+ # === Get hook name from script path ===
95
+ get_hook_name() {
96
+ local script="$1"
97
+ basename "$script" .sh | tr '/' '-'
98
+ }
99
+
61
100
  # === Read stdin with timeout ===
62
101
  # Critical: stdin can block forever if not handled properly
63
102
  read_stdin_with_timeout() {
@@ -92,18 +131,68 @@ main() {
92
131
  exit 0
93
132
  fi
94
133
 
95
- # === CRASH PREVENTION: Process Storm Detection ===
96
- # If too many hooks are running, skip this one to prevent cascade
97
- if type detect_process_storm &>/dev/null; then
98
- local storm_status
99
- storm_status=$(detect_process_storm 25)
100
- if [[ "$storm_status" == STORM* ]]; then
101
- log_failure "$script - BLOCKED due to process storm: $storm_status"
134
+ local hook_name
135
+ hook_name=$(get_hook_name "$script")
136
+
137
+ # === Initialize libraries ===
138
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
139
+ log_init "$hook_name"
140
+ log_debug "Starting hook execution" "script=$script"
141
+ fi
142
+
143
+ if [[ "$METRICS_LOADED" == "true" ]]; then
144
+ metrics_init "$hook_name"
145
+ metrics_start_request
146
+ fi
147
+
148
+ # === CIRCUIT BREAKER CHECK ===
149
+ # If circuit is open for this hook, fail fast
150
+ if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
151
+ if ! cb_allow_request "$hook_name"; then
152
+ local cb_status
153
+ cb_status=$(cb_get_status "$hook_name")
154
+ log_debug "Circuit breaker OPEN for $hook_name: $cb_status"
155
+
156
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
157
+ log_warn "Circuit breaker open - failing fast" "hook=$hook_name"
158
+ fi
159
+
160
+ if [[ "$METRICS_LOADED" == "true" ]]; then
161
+ metrics_end_request "skipped"
162
+ fi
163
+
164
+ get_safe_output "$script"
165
+ exit 0
166
+ fi
167
+ fi
168
+
169
+ # === SEMAPHORE: Acquire concurrency slot ===
170
+ local semaphore_acquired=false
171
+ if [[ "$SEMAPHORE_LOADED" == "true" ]]; then
172
+ if acquire_semaphore "hooks" "$HOOK_MAX_CONCURRENT" "$HOOK_ACQUIRE_TIMEOUT_MS"; then
173
+ semaphore_acquired=true
174
+ log_debug "Acquired semaphore slot for $hook_name"
175
+ else
176
+ # Could not acquire semaphore in time - graceful degradation
177
+ log_debug "Semaphore timeout for $hook_name - graceful skip"
178
+
179
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
180
+ log_warn "Semaphore acquisition timeout - graceful degradation" "hook=$hook_name" "max_concurrent=$HOOK_MAX_CONCURRENT"
181
+ fi
182
+
183
+ if [[ "$METRICS_LOADED" == "true" ]]; then
184
+ metrics_end_request "skipped"
185
+ fi
186
+
187
+ # DON'T record as failure - this is graceful degradation, not an error
102
188
  get_safe_output "$script"
103
189
  exit 0
104
190
  fi
105
191
  fi
106
192
 
193
+ # Ensure semaphore is released on exit
194
+ trap 'release_semaphore 2>/dev/null || true' EXIT INT TERM
195
+
107
196
  log_debug "Executing: $script (timeout: ${HOOK_TIMEOUT}s)"
108
197
 
109
198
  # Read stdin first (with its own timeout)
@@ -111,11 +200,8 @@ main() {
111
200
  stdin_content=$(read_stdin_with_timeout)
112
201
 
113
202
  # Execute the hook with hard timeout
114
- # Using timeout with --kill-after to ensure SIGKILL if SIGTERM doesn't work
115
203
  local output
116
204
  local exit_code
117
-
118
- # Create temp file for output (avoid subshell issues)
119
205
  local tmp_out
120
206
  tmp_out=$(mktemp)
121
207
 
@@ -156,12 +242,30 @@ main() {
156
242
  output=$(cat "$tmp_out" 2>/dev/null)
157
243
  rm -f "$tmp_out"
158
244
 
245
+ # Release semaphore immediately after execution
246
+ if [[ "$semaphore_acquired" == "true" ]]; then
247
+ release_semaphore
248
+ fi
249
+
159
250
  # Handle timeout (exit code 124 or 137)
160
251
  if [[ $exit_code -eq 124 ]] || [[ $exit_code -eq 137 ]]; then
161
252
  log_failure "$script - killed after ${HOOK_TIMEOUT}s"
162
253
  log_debug "TIMEOUT: $script killed after ${HOOK_TIMEOUT}s"
163
254
 
164
- # === CRASH PREVENTION: Clean up potential zombie processes ===
255
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
256
+ log_error "Hook timeout - killed after ${HOOK_TIMEOUT}s" "hook=$hook_name"
257
+ fi
258
+
259
+ if [[ "$METRICS_LOADED" == "true" ]]; then
260
+ metrics_end_request "timeout"
261
+ fi
262
+
263
+ # Record failure for circuit breaker
264
+ if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
265
+ cb_record_failure "$hook_name"
266
+ fi
267
+
268
+ # Clean up potential zombie processes (legacy)
165
269
  if type kill_zombie_heredocs &>/dev/null; then
166
270
  kill_zombie_heredocs 2>/dev/null || true
167
271
  fi
@@ -170,6 +274,36 @@ main() {
170
274
  exit 0
171
275
  fi
172
276
 
277
+ # Handle hook errors (non-zero exit, excluding block exit code 2)
278
+ if [[ $exit_code -ne 0 ]] && [[ $exit_code -ne 2 ]]; then
279
+ log_debug "Hook error: $script exited with $exit_code"
280
+
281
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
282
+ log_warn "Hook exited with error" "hook=$hook_name" "exit_code=$exit_code"
283
+ fi
284
+
285
+ if [[ "$METRICS_LOADED" == "true" ]]; then
286
+ metrics_end_request "failure"
287
+ fi
288
+
289
+ if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
290
+ cb_record_failure "$hook_name"
291
+ fi
292
+ else
293
+ # Success!
294
+ if [[ "$METRICS_LOADED" == "true" ]]; then
295
+ metrics_end_request "success"
296
+ fi
297
+
298
+ if [[ "$CIRCUIT_BREAKER_LOADED" == "true" ]]; then
299
+ cb_record_success "$hook_name"
300
+ fi
301
+
302
+ if [[ "$LOGGING_LOADED" == "true" ]]; then
303
+ log_debug "Hook completed successfully" "hook=$hook_name"
304
+ fi
305
+ fi
306
+
173
307
  # Return output or safe default
174
308
  if [[ -n "$output" ]]; then
175
309
  echo "$output"