specweave 1.0.31 → 1.0.33
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-plugin/marketplace.json +1 -1
- package/CLAUDE.md +205 -148
- package/README.md +0 -2
- package/bin/specweave.js +11 -0
- package/dist/src/cli/commands/init.js +1 -1
- package/dist/src/cli/commands/init.js.map +1 -1
- package/dist/src/cli/commands/update-instructions.d.ts +16 -0
- package/dist/src/cli/commands/update-instructions.d.ts.map +1 -0
- package/dist/src/cli/commands/update-instructions.js +134 -0
- package/dist/src/cli/commands/update-instructions.js.map +1 -0
- package/dist/src/cli/helpers/init/directory-structure.d.ts +28 -1
- package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/directory-structure.js +163 -33
- package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
- package/dist/src/cli/helpers/init/index.d.ts +2 -1
- package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
- package/dist/src/cli/helpers/init/index.js +3 -1
- package/dist/src/cli/helpers/init/index.js.map +1 -1
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts +23 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.d.ts.map +1 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js +243 -0
- package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -0
- package/dist/src/cli/helpers/init/plugin-installer.js +49 -0
- package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
- package/dist/src/config/types.d.ts +2 -2
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +26 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -1
- package/dist/src/core/living-docs/external-sync-orchestrator.js +61 -0
- package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -1
- package/dist/src/core/living-docs/scaffolding/index.d.ts +12 -0
- package/dist/src/core/living-docs/scaffolding/index.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/index.js +15 -0
- package/dist/src/core/living-docs/scaffolding/index.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts +183 -0
- package/dist/src/core/living-docs/scaffolding/merger.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/merger.js +523 -0
- package/dist/src/core/living-docs/scaffolding/merger.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts +102 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js +346 -0
- package/dist/src/core/living-docs/scaffolding/scaffold.js.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts +108 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.d.ts.map +1 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js +204 -0
- package/dist/src/core/living-docs/scaffolding/template-engine.js.map +1 -0
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts +38 -2
- package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/generators.js +65 -10
- package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
- package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
- package/dist/src/core/tools/index.d.ts +11 -0
- package/dist/src/core/tools/index.d.ts.map +1 -0
- package/dist/src/core/tools/index.js +10 -0
- package/dist/src/core/tools/index.js.map +1 -0
- package/dist/src/core/tools/tool-event-bus.d.ts +33 -0
- package/dist/src/core/tools/tool-event-bus.d.ts.map +1 -0
- package/dist/src/core/tools/tool-event-bus.js +84 -0
- package/dist/src/core/tools/tool-event-bus.js.map +1 -0
- package/dist/src/core/tools/tool-index-builder.d.ts +27 -0
- package/dist/src/core/tools/tool-index-builder.d.ts.map +1 -0
- package/dist/src/core/tools/tool-index-builder.js +289 -0
- package/dist/src/core/tools/tool-index-builder.js.map +1 -0
- package/dist/src/core/tools/tool-registry.d.ts +51 -0
- package/dist/src/core/tools/tool-registry.d.ts.map +1 -0
- package/dist/src/core/tools/tool-registry.js +224 -0
- package/dist/src/core/tools/tool-registry.js.map +1 -0
- package/dist/src/core/tools/tool-search-engine.d.ts +22 -0
- package/dist/src/core/tools/tool-search-engine.d.ts.map +1 -0
- package/dist/src/core/tools/tool-search-engine.js +174 -0
- package/dist/src/core/tools/tool-search-engine.js.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts +112 -0
- package/dist/src/core/tools/types/tool-registry-types.d.ts.map +1 -0
- package/dist/src/core/tools/types/tool-registry-types.js +7 -0
- package/dist/src/core/tools/types/tool-registry-types.js.map +1 -0
- package/dist/src/init/compliance/types.d.ts +1 -1
- package/package.json +1 -1
- package/plugins/specweave/hooks/hooks.json +3 -13
- package/plugins/specweave/hooks/lib/common-setup.sh +47 -321
- package/plugins/specweave/hooks/lib/migrate-increment-work.sh +5 -5
- package/plugins/specweave/hooks/lib/sync-spec-content.sh +5 -5
- package/plugins/specweave/hooks/universal/dispatcher.mjs +4 -5
- package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +43 -296
- package/plugins/specweave/hooks/universal/hook-wrapper.sh +3 -1
- package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
- package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +2 -2
- package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -10
- package/plugins/specweave/hooks/v2/guards/completion-guard.sh +12 -29
- package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +27 -29
- package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +10 -4
- package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +139 -0
- package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +4 -2
- package/plugins/specweave/hooks/v2/session-end.sh +3 -1
- package/plugins/specweave/hooks/v2/session-start.sh +3 -1
- package/plugins/specweave/skills/increment-planner/templates/plan.md +14 -0
- package/plugins/specweave/skills/update-instructions/SKILL.md +80 -0
- package/plugins/specweave-ado/hooks/post-living-docs-update.sh +1 -1
- package/plugins/specweave-mobile/README.md +55 -35
- package/plugins/specweave-mobile/agents/mobile-architect/AGENT.md +805 -329
- package/plugins/specweave-mobile/skills/expo-workflow/SKILL.md +226 -9
- package/plugins/specweave-mobile/skills/native-modules/SKILL.md +221 -20
- package/plugins/specweave-mobile/skills/performance-optimization/SKILL.md +186 -14
- package/plugins/specweave-mobile/skills/react-native-setup/SKILL.md +151 -54
- package/plugins/specweave-release/commands/npm.md +61 -17
- package/plugins/specweave-release/hooks/post-task-completion.sh +2 -3
- package/src/templates/AGENTS.md.template +34 -0
- package/src/templates/CLAUDE.md.template +121 -155
- package/plugins/specweave/hooks/config-env-separator.sh +0 -99
- package/plugins/specweave/hooks/github-metadata-guard.sh +0 -73
- package/plugins/specweave/hooks/lib/circuit-breaker.sh +0 -381
- package/plugins/specweave/hooks/lib/crash-prevention.sh +0 -336
- package/plugins/specweave/hooks/lib/logging.sh +0 -231
- package/plugins/specweave/hooks/lib/metrics.sh +0 -347
- package/plugins/specweave/hooks/lib/semaphore.sh +0 -216
- package/plugins/specweave/hooks/project-folder-guard.sh +0 -274
- package/plugins/specweave/hooks/spec-project-validator.sh +0 -210
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +0 -212
- package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +0 -163
- package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +0 -51
- package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +0 -63
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +0 -335
- package/plugins/specweave/hooks/v2/guards/per-us-project-validator.test.sh +0 -406
|
@@ -1,317 +1,64 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# fail-fast-wrapper.sh -
|
|
3
|
-
# If ANY hook takes longer than HOOK_TIMEOUT, it gets KILLED.
|
|
2
|
+
# fail-fast-wrapper.sh - Simple timeout wrapper for hooks
|
|
4
3
|
#
|
|
5
4
|
# Usage: bash fail-fast-wrapper.sh <hook-script> [args...]
|
|
6
5
|
#
|
|
7
6
|
# Environment:
|
|
8
|
-
# HOOK_TIMEOUT
|
|
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)
|
|
7
|
+
# HOOK_TIMEOUT - max seconds (default: 5)
|
|
12
8
|
#
|
|
13
|
-
#
|
|
14
|
-
# - Returns hook output on success
|
|
15
|
-
# - Returns safe JSON on timeout ({"continue":true} or {"decision":"approve"})
|
|
16
|
-
# - NEVER hangs - timeout is enforced with SIGKILL
|
|
17
|
-
#
|
|
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
|
|
23
|
-
#
|
|
24
|
-
# v0.33.0 - Enhanced with crash prevention integration
|
|
25
|
-
# v1.0.30 - Complete rewrite with proper concurrency primitives
|
|
26
|
-
|
|
27
|
-
set -o pipefail
|
|
28
|
-
|
|
29
|
-
# === Configuration ===
|
|
30
|
-
HOOK_TIMEOUT="${HOOK_TIMEOUT:-5}" # 5 seconds - more than enough for any hook
|
|
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
|
|
34
|
-
LOG_FILE="${HOME}/.claude/hook-failures.log"
|
|
35
|
-
|
|
36
|
-
# === Library paths ===
|
|
37
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
38
|
-
LIB_DIR="${SCRIPT_DIR}/../lib"
|
|
39
|
-
|
|
40
|
-
# === Source libraries (fail gracefully if missing) ===
|
|
41
|
-
SEMAPHORE_LOADED=false
|
|
42
|
-
CIRCUIT_BREAKER_LOADED=false
|
|
43
|
-
LOGGING_LOADED=false
|
|
44
|
-
METRICS_LOADED=false
|
|
9
|
+
# Returns safe JSON on timeout or error. Never hangs.
|
|
45
10
|
|
|
46
|
-
|
|
47
|
-
export SPECWEAVE_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}"
|
|
48
|
-
export SPECWEAVE_LOG_DIR="${SPECWEAVE_LOG_DIR:-.specweave/logs/hooks}"
|
|
11
|
+
set +e
|
|
49
12
|
|
|
50
|
-
|
|
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
|
|
13
|
+
HOOK_TIMEOUT="${HOOK_TIMEOUT:-5}"
|
|
61
14
|
|
|
62
|
-
|
|
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"
|
|
68
|
-
if [[ -f "$CRASH_PREVENTION" ]]; then
|
|
69
|
-
source "$CRASH_PREVENTION" 2>/dev/null || true
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
# === Helper functions ===
|
|
73
|
-
log_debug() {
|
|
74
|
-
[[ "$HOOK_DEBUG" == "1" ]] && echo "[DEBUG $(date +%H:%M:%S)] $*" >&2
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
log_failure() {
|
|
78
|
-
local msg="$1"
|
|
79
|
-
mkdir -p "$(dirname "$LOG_FILE")"
|
|
80
|
-
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] HOOK FAILURE: $msg" >> "$LOG_FILE"
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# === Safe JSON output based on hook type ===
|
|
15
|
+
# Safe output based on hook type
|
|
84
16
|
get_safe_output() {
|
|
85
17
|
local script="$1"
|
|
86
|
-
|
|
87
|
-
if [[ "$script" == *"guard"* ]] || [[ "$script" == *"validator"* ]] || [[ "$script" == *"PreToolUse"* ]]; then
|
|
18
|
+
if [[ "$script" == *"guard"* ]] || [[ "$script" == *"validator"* ]]; then
|
|
88
19
|
echo '{"decision":"allow"}'
|
|
89
20
|
else
|
|
90
21
|
echo '{"continue":true}'
|
|
91
22
|
fi
|
|
92
23
|
}
|
|
93
24
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
#
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# === Main execution ===
|
|
118
|
-
main() {
|
|
119
|
-
local script="$1"
|
|
120
|
-
shift
|
|
121
|
-
local args=("$@")
|
|
122
|
-
|
|
123
|
-
if [[ -z "$script" ]]; then
|
|
124
|
-
echo '{"continue":true}'
|
|
125
|
-
exit 0
|
|
126
|
-
fi
|
|
127
|
-
|
|
128
|
-
if [[ ! -f "$script" ]]; then
|
|
129
|
-
log_debug "Script not found: $script"
|
|
130
|
-
echo '{"continue":true}'
|
|
131
|
-
exit 0
|
|
132
|
-
fi
|
|
133
|
-
|
|
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
|
|
188
|
-
get_safe_output "$script"
|
|
189
|
-
exit 0
|
|
190
|
-
fi
|
|
191
|
-
fi
|
|
192
|
-
|
|
193
|
-
# Ensure semaphore is released on exit
|
|
194
|
-
trap 'release_semaphore 2>/dev/null || true' EXIT INT TERM
|
|
195
|
-
|
|
196
|
-
log_debug "Executing: $script (timeout: ${HOOK_TIMEOUT}s)"
|
|
197
|
-
|
|
198
|
-
# Read stdin first (with its own timeout)
|
|
199
|
-
local stdin_content
|
|
200
|
-
stdin_content=$(read_stdin_with_timeout)
|
|
201
|
-
|
|
202
|
-
# Execute the hook with hard timeout
|
|
203
|
-
local output
|
|
204
|
-
local exit_code
|
|
205
|
-
local tmp_out
|
|
206
|
-
tmp_out=$(mktemp)
|
|
207
|
-
|
|
208
|
-
# Run with timeout - kill entire process group on timeout
|
|
209
|
-
if command -v gtimeout >/dev/null 2>&1; then
|
|
210
|
-
# macOS with coreutils
|
|
211
|
-
echo "$stdin_content" | gtimeout --kill-after=2 "$HOOK_TIMEOUT" bash "$script" "${args[@]}" > "$tmp_out" 2>/dev/null
|
|
212
|
-
exit_code=$?
|
|
213
|
-
elif command -v timeout >/dev/null 2>&1; then
|
|
214
|
-
# Linux
|
|
215
|
-
echo "$stdin_content" | timeout --kill-after=2 "$HOOK_TIMEOUT" bash "$script" "${args[@]}" > "$tmp_out" 2>/dev/null
|
|
216
|
-
exit_code=$?
|
|
217
|
-
else
|
|
218
|
-
# Fallback: manual timeout using background process
|
|
219
|
-
(
|
|
220
|
-
echo "$stdin_content" | bash "$script" "${args[@]}" > "$tmp_out" 2>/dev/null
|
|
221
|
-
) &
|
|
222
|
-
local pid=$!
|
|
223
|
-
|
|
224
|
-
# Wait with timeout
|
|
225
|
-
local count=0
|
|
226
|
-
while kill -0 "$pid" 2>/dev/null && [[ $count -lt $((HOOK_TIMEOUT * 10)) ]]; do
|
|
227
|
-
sleep 0.1
|
|
228
|
-
count=$((count + 1))
|
|
229
|
-
done
|
|
230
|
-
|
|
231
|
-
if kill -0 "$pid" 2>/dev/null; then
|
|
232
|
-
# Still running - kill it!
|
|
233
|
-
kill -9 "$pid" 2>/dev/null
|
|
234
|
-
wait "$pid" 2>/dev/null
|
|
235
|
-
exit_code=124 # timeout exit code
|
|
236
|
-
else
|
|
237
|
-
wait "$pid"
|
|
238
|
-
exit_code=$?
|
|
239
|
-
fi
|
|
240
|
-
fi
|
|
241
|
-
|
|
242
|
-
output=$(cat "$tmp_out" 2>/dev/null)
|
|
243
|
-
rm -f "$tmp_out"
|
|
244
|
-
|
|
245
|
-
# Release semaphore immediately after execution
|
|
246
|
-
if [[ "$semaphore_acquired" == "true" ]]; then
|
|
247
|
-
release_semaphore
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
# Handle timeout (exit code 124 or 137)
|
|
251
|
-
if [[ $exit_code -eq 124 ]] || [[ $exit_code -eq 137 ]]; then
|
|
252
|
-
log_failure "$script - killed after ${HOOK_TIMEOUT}s"
|
|
253
|
-
log_debug "TIMEOUT: $script killed after ${HOOK_TIMEOUT}s"
|
|
254
|
-
|
|
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)
|
|
269
|
-
if type kill_zombie_heredocs &>/dev/null; then
|
|
270
|
-
kill_zombie_heredocs 2>/dev/null || true
|
|
271
|
-
fi
|
|
272
|
-
|
|
273
|
-
get_safe_output "$script"
|
|
274
|
-
exit 0
|
|
275
|
-
fi
|
|
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
|
|
25
|
+
script="$1"
|
|
26
|
+
shift
|
|
27
|
+
|
|
28
|
+
# No script or doesn't exist = safe default
|
|
29
|
+
[[ -z "$script" || ! -f "$script" ]] && echo '{"continue":true}' && exit 0
|
|
30
|
+
|
|
31
|
+
# Read stdin
|
|
32
|
+
stdin_content=$(cat 2>/dev/null || echo '{}')
|
|
33
|
+
|
|
34
|
+
# Run with timeout
|
|
35
|
+
tmp_out=$(mktemp)
|
|
36
|
+
if command -v gtimeout >/dev/null 2>&1; then
|
|
37
|
+
echo "$stdin_content" | gtimeout --kill-after=2 "$HOOK_TIMEOUT" bash "$script" "$@" > "$tmp_out" 2>/dev/null
|
|
38
|
+
exit_code=$?
|
|
39
|
+
elif command -v timeout >/dev/null 2>&1; then
|
|
40
|
+
echo "$stdin_content" | timeout --kill-after=2 "$HOOK_TIMEOUT" bash "$script" "$@" > "$tmp_out" 2>/dev/null
|
|
41
|
+
exit_code=$?
|
|
42
|
+
else
|
|
43
|
+
# Fallback: run without timeout (rare - most systems have timeout)
|
|
44
|
+
echo "$stdin_content" | bash "$script" "$@" > "$tmp_out" 2>/dev/null
|
|
45
|
+
exit_code=$?
|
|
46
|
+
fi
|
|
306
47
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
echo "$output"
|
|
310
|
-
else
|
|
311
|
-
get_safe_output "$script"
|
|
312
|
-
fi
|
|
48
|
+
output=$(cat "$tmp_out" 2>/dev/null)
|
|
49
|
+
rm -f "$tmp_out"
|
|
313
50
|
|
|
51
|
+
# Timeout = safe default
|
|
52
|
+
if [[ $exit_code -eq 124 ]] || [[ $exit_code -eq 137 ]]; then
|
|
53
|
+
get_safe_output "$script"
|
|
314
54
|
exit 0
|
|
315
|
-
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
# Return output or safe default
|
|
58
|
+
if [[ -n "$output" ]]; then
|
|
59
|
+
echo "$output"
|
|
60
|
+
else
|
|
61
|
+
get_safe_output "$script"
|
|
62
|
+
fi
|
|
316
63
|
|
|
317
|
-
|
|
64
|
+
exit 0
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
#
|
|
9
9
|
# Usage: bash hook-wrapper.sh <hook-type>
|
|
10
10
|
# Where hook-type is: session-start, post-tool-use, completion-guard
|
|
11
|
+
#
|
|
12
|
+
# v0.35.3 - Fixed: use set +e for hook safety
|
|
11
13
|
|
|
12
|
-
set -e
|
|
14
|
+
set +e # CRITICAL: Never use set -e in hooks (causes cascading failures)
|
|
13
15
|
|
|
14
16
|
HOOK_TYPE="${1:-unknown}"
|
|
15
17
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
@@ -18,7 +18,7 @@ set +e
|
|
|
18
18
|
# ==============================================================================
|
|
19
19
|
# ULTRA-FAST EARLY EXIT (before ANY processing)
|
|
20
20
|
# ==============================================================================
|
|
21
|
-
INPUT=$(cat)
|
|
21
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
22
22
|
|
|
23
23
|
# Use jq if available (10x faster than node), fallback to simple grep
|
|
24
24
|
if command -v jq >/dev/null 2>&1; then
|
|
@@ -26,8 +26,8 @@ while [[ "$PROJECT_ROOT" != "/" ]] && [[ ! -d "$PROJECT_ROOT/.specweave" ]]; do
|
|
|
26
26
|
done
|
|
27
27
|
[[ ! -d "$PROJECT_ROOT/.specweave" ]] && exit 0
|
|
28
28
|
|
|
29
|
-
# Read stdin (tool result JSON)
|
|
30
|
-
INPUT=$(cat)
|
|
29
|
+
# Read stdin (tool result JSON) - with safe fallback to prevent hanging
|
|
30
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
31
31
|
|
|
32
32
|
# Extract file path (fast grep, no jq)
|
|
33
33
|
FILE_PATH=$(echo "$INPUT" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)".*/\1/')
|
|
@@ -15,17 +15,8 @@ done
|
|
|
15
15
|
# Consume stdin
|
|
16
16
|
cat > /dev/null
|
|
17
17
|
|
|
18
|
-
#
|
|
19
|
-
# Check that essential guards are available to prevent session hangs
|
|
18
|
+
# Hook directory for finding other scripts
|
|
20
19
|
HOOK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
-
GUARDS_DIR="$HOOK_DIR/../guards"
|
|
22
|
-
BASH_FILE_GUARD="$GUARDS_DIR/bash-file-guard.sh"
|
|
23
|
-
|
|
24
|
-
if [[ ! -f "$BASH_FILE_GUARD" ]] || [[ ! -x "$BASH_FILE_GUARD" ]]; then
|
|
25
|
-
# Output warning as system message
|
|
26
|
-
echo '{"continue":true,"systemMessage":"⚠️ CRITICAL: bash-file-guard.sh not found or not executable! Session hang protection DISABLED. Run: bash scripts/refresh-marketplace.sh"}'
|
|
27
|
-
# Continue session but warn about missing protection
|
|
28
|
-
fi
|
|
29
20
|
|
|
30
21
|
# === MCP Connection Health Check ===
|
|
31
22
|
# Check for recent MCP drops that could cause session hangs
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
# PreToolUse hook - can BLOCK the tool call by returning non-zero exit code
|
|
8
8
|
#
|
|
9
9
|
# IMPORTANT: This is a safety guard. Exit 0 allows, exit 2 blocks.
|
|
10
|
+
# All exit paths MUST output JSON for proper Claude Code handling.
|
|
10
11
|
set +e
|
|
11
12
|
|
|
12
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
|
|
13
|
+
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
13
14
|
|
|
14
15
|
# Read stdin for tool input
|
|
15
|
-
INPUT=$(cat)
|
|
16
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
16
17
|
|
|
17
18
|
# Check if this is editing metadata.json with status: completed
|
|
18
19
|
# Pattern: file_path contains metadata.json AND (new_string OR content) contains "status"..."completed"
|
|
@@ -27,6 +28,7 @@ fi
|
|
|
27
28
|
|
|
28
29
|
# Only care about metadata.json files
|
|
29
30
|
if [[ "$FILE_PATH" != *metadata.json ]]; then
|
|
31
|
+
echo '{"decision":"allow"}'
|
|
30
32
|
exit 0 # Allow
|
|
31
33
|
fi
|
|
32
34
|
|
|
@@ -50,40 +52,21 @@ if echo "$NEW_CONTENT" | grep -q '"status"[[:space:]]*:[[:space:]]*"completed"';
|
|
|
50
52
|
|
|
51
53
|
if [[ "$CURRENT_STATUS" == "ready_for_review" ]]; then
|
|
52
54
|
# This is a valid transition - allow
|
|
55
|
+
echo '{"decision":"allow"}'
|
|
53
56
|
exit 0
|
|
54
57
|
fi
|
|
55
58
|
fi
|
|
56
59
|
|
|
57
60
|
# BLOCK - trying to set completed without going through ready_for_review
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
echo ""
|
|
65
|
-
echo "This prevents the auto-completion bug where increments get marked as"
|
|
66
|
-
echo "completed without proper validation."
|
|
67
|
-
echo ""
|
|
68
|
-
echo "CORRECT WORKFLOW:"
|
|
69
|
-
echo "1. All tasks completed -> status auto-transitions to \"ready_for_review\""
|
|
70
|
-
echo "2. Run /sw:done <increment-id> with explicit user confirmation"
|
|
71
|
-
echo "3. Only then does status become \"completed\""
|
|
72
|
-
echo ""
|
|
73
|
-
echo "WHY THIS MATTERS:"
|
|
74
|
-
echo "- Ensures all ACs are checked in spec.md before closure"
|
|
75
|
-
echo "- Requires explicit user approval"
|
|
76
|
-
echo "- Maintains audit trail (approvedAt timestamp)"
|
|
77
|
-
echo ""
|
|
78
|
-
echo "If you're implementing closure logic, use:"
|
|
79
|
-
echo " MetadataManager.updateStatus(incrementId, IncrementStatus.COMPLETED)"
|
|
80
|
-
echo ""
|
|
81
|
-
echo "This will only succeed if current status is \"ready_for_review\"."
|
|
82
|
-
echo ""
|
|
83
|
-
echo "=============================================================================="
|
|
84
|
-
|
|
61
|
+
cat << 'BLOCK_EOF'
|
|
62
|
+
{
|
|
63
|
+
"decision": "block",
|
|
64
|
+
"reason": "🚫 BLOCKED: Direct status change to \"completed\" is not allowed (v0.28.63+)\n\nYou cannot directly set status to \"completed\" in metadata.json.\n\nCORRECT WORKFLOW:\n1. All tasks completed → status auto-transitions to \"ready_for_review\"\n2. Run /sw:done <increment-id> with explicit user confirmation\n3. Only then does status become \"completed\"\n\nWHY THIS MATTERS:\n• Ensures all ACs are checked in spec.md before closure\n• Requires explicit user approval\n• Maintains audit trail (approvedAt timestamp)\n\n🔧 If implementing closure logic, use:\n MetadataManager.updateStatus(incrementId, IncrementStatus.COMPLETED)"
|
|
65
|
+
}
|
|
66
|
+
BLOCK_EOF
|
|
85
67
|
exit 2 # Block the tool call
|
|
86
68
|
fi
|
|
87
69
|
|
|
88
70
|
# Allow other edits to metadata.json
|
|
71
|
+
echo '{"decision":"allow"}'
|
|
89
72
|
exit 0
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
# - 0121-ado-jira-feature-parity-p2-p3
|
|
11
11
|
# - 0121-intelligent-living-docs-content
|
|
12
12
|
#
|
|
13
|
-
# Exit 0 = allow, Exit 2 = block
|
|
13
|
+
# Exit 0 = allow (with JSON), Exit 2 = block (with JSON)
|
|
14
14
|
set +e
|
|
15
15
|
|
|
16
|
-
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && exit 0
|
|
16
|
+
[[ "${SPECWEAVE_DISABLE_HOOKS:-0}" == "1" ]] && echo '{"decision":"allow"}' && exit 0
|
|
17
17
|
|
|
18
18
|
# Read stdin for tool input
|
|
19
|
-
INPUT=$(cat)
|
|
19
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
20
20
|
|
|
21
21
|
# Extract file_path from the tool call
|
|
22
22
|
# Claude Code passes tool input in .tool_input.file_path format
|
|
@@ -28,6 +28,7 @@ fi
|
|
|
28
28
|
|
|
29
29
|
# Only care about .specweave/increments/ paths
|
|
30
30
|
if [[ "$FILE_PATH" != *.specweave/increments/* ]]; then
|
|
31
|
+
echo '{"decision":"allow"}'
|
|
31
32
|
exit 0 # Not an increment file - allow
|
|
32
33
|
fi
|
|
33
34
|
|
|
@@ -43,6 +44,7 @@ INCREMENT_FOLDER=$(echo "$AFTER_INCREMENTS" | cut -d'/' -f1)
|
|
|
43
44
|
|
|
44
45
|
# Skip special folders
|
|
45
46
|
if [[ "$INCREMENT_FOLDER" == "_archive" ]] || [[ "$INCREMENT_FOLDER" == "_abandoned" ]] || [[ "$INCREMENT_FOLDER" == "_paused" ]] || [[ "$INCREMENT_FOLDER" == "README.md" ]]; then
|
|
47
|
+
echo '{"decision":"allow"}'
|
|
46
48
|
exit 0
|
|
47
49
|
fi
|
|
48
50
|
|
|
@@ -50,6 +52,7 @@ fi
|
|
|
50
52
|
INCREMENT_NUM=$(echo "$INCREMENT_FOLDER" | grep -oE '^[0-9]{3,4}' | head -1)
|
|
51
53
|
|
|
52
54
|
if [[ -z "$INCREMENT_NUM" ]]; then
|
|
55
|
+
echo '{"decision":"allow"}'
|
|
53
56
|
exit 0 # Not a standard increment folder pattern - allow
|
|
54
57
|
fi
|
|
55
58
|
|
|
@@ -60,7 +63,9 @@ INCREMENT_NUM=$(printf "%04d" "$((10#$INCREMENT_NUM))")
|
|
|
60
63
|
INCREMENTS_DIR=$(echo "$FILE_PATH" | grep -o '.*/\.specweave/increments' | head -1)
|
|
61
64
|
|
|
62
65
|
if [[ ! -d "$INCREMENTS_DIR" ]]; then
|
|
63
|
-
|
|
66
|
+
# Increments directory doesn't exist yet - first increment, allow creation
|
|
67
|
+
echo '{"decision":"allow"}'
|
|
68
|
+
exit 0
|
|
64
69
|
fi
|
|
65
70
|
|
|
66
71
|
# Scan ALL directories for existing increment with the same number
|
|
@@ -104,37 +109,30 @@ for DIR in "${DIRS_TO_CHECK[@]}"; do
|
|
|
104
109
|
done < <(find "$DIR" -maxdepth 1 -type d -name "${INCREMENT_NUM}*" -print0 2>/dev/null)
|
|
105
110
|
done
|
|
106
111
|
|
|
107
|
-
# If duplicates found, BLOCK the operation
|
|
112
|
+
# If duplicates found, BLOCK the operation and provide the correct number to use
|
|
108
113
|
if [[ ${#FOUND_DUPLICATES[@]} -gt 0 ]]; then
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
echo " BLOCKED: Duplicate increment ID detected (v0.33.0+)"
|
|
112
|
-
echo "=============================================================================="
|
|
113
|
-
echo ""
|
|
114
|
-
echo "You are trying to create increment: $INCREMENT_FOLDER"
|
|
115
|
-
echo "But increment number $INCREMENT_NUM already exists:"
|
|
116
|
-
echo ""
|
|
114
|
+
# Format duplicates for JSON
|
|
115
|
+
DUP_LIST=""
|
|
117
116
|
for DUP in "${FOUND_DUPLICATES[@]}"; do
|
|
118
|
-
|
|
117
|
+
DUP_LIST="${DUP_LIST}\\n - ${DUP}"
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
# Calculate the next available number using gap-filling strategy
|
|
121
|
+
EXISTING_NUMS=$(find "$INCREMENTS_DIR" "$INCREMENTS_DIR/_archive" "$INCREMENTS_DIR/_abandoned" "$INCREMENTS_DIR/_paused" -maxdepth 1 -type d -name "[0-9]*-*" 2>/dev/null | xargs -I {} basename {} | grep -oE '^[0-9]{3,4}' | sort -n | uniq)
|
|
122
|
+
NEXT_NUM=1
|
|
123
|
+
while echo "$EXISTING_NUMS" | grep -q "^$(printf "%04d" $NEXT_NUM)$\|^$(printf "%03d" $NEXT_NUM)$"; do
|
|
124
|
+
NEXT_NUM=$((NEXT_NUM + 1))
|
|
119
125
|
done
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
echo "
|
|
124
|
-
|
|
125
|
-
echo " - Paused increments (_paused/)"
|
|
126
|
-
echo ""
|
|
127
|
-
echo "NOTE: 0001 and 0001E share the SAME base number and cannot coexist!"
|
|
128
|
-
echo ""
|
|
129
|
-
echo "TO FIX:"
|
|
130
|
-
echo "1. Use a different increment number"
|
|
131
|
-
echo "2. Get the next available number:"
|
|
132
|
-
echo " node -e \"import('./dist/core/increment/increment-utils.js').then(m => console.log(m.IncrementNumberManager.getNextIncrementNumber()))\""
|
|
133
|
-
echo ""
|
|
134
|
-
echo "=============================================================================="
|
|
126
|
+
NEXT_NUM_PADDED=$(printf "%04d" $NEXT_NUM)
|
|
127
|
+
|
|
128
|
+
# Extract the name part from the attempted folder
|
|
129
|
+
FOLDER_NAME_PART=$(echo "$INCREMENT_FOLDER" | sed 's/^[0-9]\{3,4\}E\?-//')
|
|
130
|
+
SUGGESTED_FOLDER="${NEXT_NUM_PADDED}-${FOLDER_NAME_PART}"
|
|
135
131
|
|
|
132
|
+
printf '{"decision":"block","reason":"🚫 DUPLICATE INCREMENT ID - Use this instead:\\n\\n✅ CORRECT: %s\\n❌ ATTEMPTED: %s\\n\\nNumber %s already exists:%s\\n\\n🔧 IMMEDIATE FIX: Replace your file path with:\\n .specweave/increments/%s/...\\n\\nThe guard calculated the next available number for you."}\n' "$SUGGESTED_FOLDER" "$INCREMENT_FOLDER" "$INCREMENT_NUM" "$DUP_LIST" "$SUGGESTED_FOLDER"
|
|
136
133
|
exit 2 # Block the tool call
|
|
137
134
|
fi
|
|
138
135
|
|
|
139
136
|
# No duplicates - allow
|
|
137
|
+
echo '{"decision":"allow"}'
|
|
140
138
|
exit 0
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
#
|
|
26
26
|
# v0.34.0 - Initial implementation based on user project bug analysis
|
|
27
27
|
|
|
28
|
-
set -e
|
|
28
|
+
set +e # CRITICAL: Never use set -e in hooks (causes cascading failures)
|
|
29
29
|
|
|
30
30
|
# Check for force bypass
|
|
31
31
|
if [ "$SPECWEAVE_FORCE_METADATA" = "1" ]; then
|
|
@@ -39,10 +39,16 @@ if [ "$SPECWEAVE_DISABLE_HOOKS" = "1" ]; then
|
|
|
39
39
|
exit 0
|
|
40
40
|
fi
|
|
41
41
|
|
|
42
|
-
# Read tool input from stdin
|
|
43
|
-
INPUT=$(cat)
|
|
42
|
+
# Read tool input from stdin (safe handling)
|
|
43
|
+
INPUT=$(cat 2>/dev/null || echo '{}')
|
|
44
44
|
|
|
45
|
-
#
|
|
45
|
+
# Check jq availability - allow if not present
|
|
46
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
47
|
+
echo '{"decision": "allow"}'
|
|
48
|
+
exit 0
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# Extract tool name - with jq fallback
|
|
46
52
|
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // .tool_input.tool_name // ""' 2>/dev/null || echo "")
|
|
47
53
|
|
|
48
54
|
# Only validate Write tool calls
|