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,99 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# Pre-Tool-Use Hook: Config-Env Separator
|
|
4
|
-
#
|
|
5
|
-
# Blocks Write/Edit to src/ files that read config values from process.env
|
|
6
|
-
# Configuration (domain, org, project) should come from ConfigManager (config.json)
|
|
7
|
-
# Only secrets (PAT, tokens, emails) should use process.env
|
|
8
|
-
#
|
|
9
|
-
# Ref: ADR-0194 - Enforce Config JSON Separation
|
|
10
|
-
#
|
|
11
|
-
|
|
12
|
-
set -e
|
|
13
|
-
|
|
14
|
-
# Only run for Write and Edit tools
|
|
15
|
-
if [[ "$TOOL_NAME" != "Write" ]] && [[ "$TOOL_NAME" != "Edit" ]]; then
|
|
16
|
-
exit 0
|
|
17
|
-
fi
|
|
18
|
-
|
|
19
|
-
# Parse tool input
|
|
20
|
-
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
|
|
21
|
-
CONTENT=$(echo "$TOOL_INPUT" | jq -r '.content // .new_string // empty')
|
|
22
|
-
|
|
23
|
-
# Skip if no file path
|
|
24
|
-
if [[ -z "$FILE_PATH" ]]; then
|
|
25
|
-
exit 0
|
|
26
|
-
fi
|
|
27
|
-
|
|
28
|
-
# Only check src/ files (not tests, plugins, etc.)
|
|
29
|
-
if [[ ! "$FILE_PATH" =~ /src/ ]]; then
|
|
30
|
-
exit 0
|
|
31
|
-
fi
|
|
32
|
-
|
|
33
|
-
# Skip test files
|
|
34
|
-
if [[ "$FILE_PATH" =~ \.test\.ts$ ]]; then
|
|
35
|
-
exit 0
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# Skip spec files
|
|
39
|
-
if [[ "$FILE_PATH" =~ \.spec\.ts$ ]]; then
|
|
40
|
-
exit 0
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
# Config variables that should NOT be in process.env
|
|
44
|
-
CONFIG_VARS=(
|
|
45
|
-
"JIRA_DOMAIN"
|
|
46
|
-
"JIRA_BASE_URL"
|
|
47
|
-
"AZURE_DEVOPS_ORG"
|
|
48
|
-
"AZURE_DEVOPS_PROJECT"
|
|
49
|
-
"GITHUB_OWNER"
|
|
50
|
-
"GITHUB_REPO"
|
|
51
|
-
"ADO_ORG_URL"
|
|
52
|
-
"ADO_PROJECT"
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Check for violations
|
|
56
|
-
VIOLATIONS=""
|
|
57
|
-
for VAR in "${CONFIG_VARS[@]}"; do
|
|
58
|
-
if echo "$CONTENT" | grep -q "process\.env\.$VAR"; then
|
|
59
|
-
VIOLATIONS="$VIOLATIONS\n - process.env.$VAR"
|
|
60
|
-
fi
|
|
61
|
-
done
|
|
62
|
-
|
|
63
|
-
# If violations found, block the operation
|
|
64
|
-
if [[ -n "$VIOLATIONS" ]]; then
|
|
65
|
-
echo ""
|
|
66
|
-
echo "-------------------------------------------------------------------"
|
|
67
|
-
echo "BLOCKED: Configuration values MUST use ConfigManager, not process.env"
|
|
68
|
-
echo "-------------------------------------------------------------------"
|
|
69
|
-
echo ""
|
|
70
|
-
echo "File: $FILE_PATH"
|
|
71
|
-
echo ""
|
|
72
|
-
echo "Violations detected:"
|
|
73
|
-
echo -e "$VIOLATIONS"
|
|
74
|
-
echo ""
|
|
75
|
-
echo "CORRECT PATTERN:"
|
|
76
|
-
echo " const config = await this.configManager.read();"
|
|
77
|
-
echo " const domain = config.issueTracker?.domain || '';"
|
|
78
|
-
echo " const org = config.issueTracker?.organization_ado || '';"
|
|
79
|
-
echo ""
|
|
80
|
-
echo "FORBIDDEN PATTERN:"
|
|
81
|
-
echo " const domain = process.env.JIRA_DOMAIN; // VIOLATION!"
|
|
82
|
-
echo " const org = process.env.AZURE_DEVOPS_ORG; // VIOLATION!"
|
|
83
|
-
echo ""
|
|
84
|
-
echo "Ref: ADR-0194, CLAUDE.md Configuration section"
|
|
85
|
-
echo ""
|
|
86
|
-
echo "To bypass (EMERGENCY ONLY): SPECWEAVE_SKIP_CONFIG_CHECK=1"
|
|
87
|
-
echo "-------------------------------------------------------------------"
|
|
88
|
-
echo ""
|
|
89
|
-
|
|
90
|
-
# Allow bypass for emergencies
|
|
91
|
-
if [[ "$SPECWEAVE_SKIP_CONFIG_CHECK" == "1" ]]; then
|
|
92
|
-
echo "WARNING: SPECWEAVE_SKIP_CONFIG_CHECK=1 - Bypassing config check!"
|
|
93
|
-
exit 0
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
exit 1
|
|
97
|
-
fi
|
|
98
|
-
|
|
99
|
-
exit 0
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
# github-metadata-guard.sh - Prevents redundant metadata headers in GitHub issue bodies
|
|
3
|
-
# WHY: GitHub has NATIVE fields (labels, milestones) - metadata belongs there, NOT in body
|
|
4
|
-
# See: .specweave/docs/internal/troubleshooting/CRITICAL-remove-metadata-header-from-github-issues.md
|
|
5
|
-
|
|
6
|
-
# Activation: PreToolUse event for Write/Edit tools
|
|
7
|
-
# Blocks: Any attempt to add metadata header (Feature, Status, Priority, Project) to GitHub issue body builders
|
|
8
|
-
|
|
9
|
-
# Exit codes:
|
|
10
|
-
# 0 = Allow operation (no metadata header detected)
|
|
11
|
-
# 1 = Block operation (metadata header detected)
|
|
12
|
-
|
|
13
|
-
# Only validate Write/Edit operations in GitHub plugin files
|
|
14
|
-
if [[ "$TOOL_NAME" != "Write" && "$TOOL_NAME" != "Edit" ]]; then
|
|
15
|
-
exit 0
|
|
16
|
-
fi
|
|
17
|
-
|
|
18
|
-
# Only check GitHub issue body builder files
|
|
19
|
-
if [[ ! "$file_path" =~ plugins/specweave-github/lib/.*issue.*builder\.ts$ ]]; then
|
|
20
|
-
exit 0
|
|
21
|
-
fi
|
|
22
|
-
|
|
23
|
-
# Get content to check (new_string for Edit, content for Write)
|
|
24
|
-
content_to_check=""
|
|
25
|
-
if [[ "$TOOL_NAME" == "Edit" ]]; then
|
|
26
|
-
content_to_check="$new_string"
|
|
27
|
-
elif [[ "$TOOL_NAME" == "Write" ]]; then
|
|
28
|
-
content_to_check="$content"
|
|
29
|
-
fi
|
|
30
|
-
|
|
31
|
-
# Check for metadata header patterns (case-insensitive)
|
|
32
|
-
if echo "$content_to_check" | grep -qiE '^\*\*Feature\*\*:|^\*\*Status\*\*:|^\*\*Priority\*\*:|^\*\*Project\*\*:'; then
|
|
33
|
-
echo "❌ BLOCKED: GitHub issue body MUST NOT contain metadata header!" >&2
|
|
34
|
-
echo "" >&2
|
|
35
|
-
echo "WHY: GitHub has NATIVE fields for metadata:" >&2
|
|
36
|
-
echo " - Feature → Milestone" >&2
|
|
37
|
-
echo " - Status → Label (status:*)" >&2
|
|
38
|
-
echo " - Priority → Label (p1, p2, p3)" >&2
|
|
39
|
-
echo " - Project → Label (project:*)" >&2
|
|
40
|
-
echo "" >&2
|
|
41
|
-
echo "Body should contain ONLY actual work content:" >&2
|
|
42
|
-
echo " ✅ ## Progress" >&2
|
|
43
|
-
echo " ✅ ## User Story" >&2
|
|
44
|
-
echo " ✅ ## Acceptance Criteria" >&2
|
|
45
|
-
echo " ✅ ## Tasks" >&2
|
|
46
|
-
echo "" >&2
|
|
47
|
-
echo "See: .specweave/docs/internal/troubleshooting/CRITICAL-remove-metadata-header-from-github-issues.md" >&2
|
|
48
|
-
exit 1
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# Check for metadata at start of body string assignments
|
|
52
|
-
if echo "$content_to_check" | grep -qE 'body \+= `\*\*(Feature|Status|Priority|Project)\*\*:'; then
|
|
53
|
-
echo "❌ BLOCKED: Metadata header assignment detected in GitHub issue body builder!" >&2
|
|
54
|
-
echo "" >&2
|
|
55
|
-
echo "Detected pattern: body += \`**Feature**:\` or similar" >&2
|
|
56
|
-
echo "" >&2
|
|
57
|
-
echo "FORBIDDEN CODE:" >&2
|
|
58
|
-
echo " body += \`**Feature**: \${featureId}\`;" >&2
|
|
59
|
-
echo " body += \`**Status**: \${status}\`;" >&2
|
|
60
|
-
echo " body += \`**Priority**: \${priority}\`;" >&2
|
|
61
|
-
echo " body += \`**Project**: \${project}\`;" >&2
|
|
62
|
-
echo "" >&2
|
|
63
|
-
echo "Use labels instead:" >&2
|
|
64
|
-
echo " labels.push(\`status:\${status}\`);" >&2
|
|
65
|
-
echo " labels.push(priority.toLowerCase());" >&2
|
|
66
|
-
echo " labels.push(\`project:\${project}\`);" >&2
|
|
67
|
-
echo "" >&2
|
|
68
|
-
echo "See: .specweave/docs/internal/troubleshooting/CRITICAL-remove-metadata-header-from-github-issues.md" >&2
|
|
69
|
-
exit 1
|
|
70
|
-
fi
|
|
71
|
-
|
|
72
|
-
# Allow operation (no metadata header detected)
|
|
73
|
-
exit 0
|
|
@@ -1,381 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# circuit-breaker.sh - Proper Circuit Breaker Pattern for SpecWeave Hooks
|
|
3
|
-
#
|
|
4
|
-
# PROBLEM SOLVED:
|
|
5
|
-
# The old circuit breaker was too simple - it just counted failures and blocked everything.
|
|
6
|
-
# This implementation follows the proper circuit breaker pattern with three states:
|
|
7
|
-
# - CLOSED: Normal operation, requests flow through
|
|
8
|
-
# - OPEN: Too many failures, requests are rejected immediately (fail-fast)
|
|
9
|
-
# - HALF-OPEN: Testing if system recovered, allows limited requests
|
|
10
|
-
#
|
|
11
|
-
# DESIGN:
|
|
12
|
-
# - Failure threshold before opening (default: 5 failures in 60s window)
|
|
13
|
-
# - Recovery timeout before half-open (default: 30s)
|
|
14
|
-
# - Success threshold to close (default: 3 successes in half-open)
|
|
15
|
-
# - Per-hook circuit breakers (not global)
|
|
16
|
-
# - Sliding window for failure counting
|
|
17
|
-
# - Automatic state transitions
|
|
18
|
-
#
|
|
19
|
-
# USAGE:
|
|
20
|
-
# source circuit-breaker.sh
|
|
21
|
-
#
|
|
22
|
-
# # Before executing hook:
|
|
23
|
-
# if cb_allow_request "hook-name"; then
|
|
24
|
-
# # Execute hook
|
|
25
|
-
# if hook_succeeded; then
|
|
26
|
-
# cb_record_success "hook-name"
|
|
27
|
-
# else
|
|
28
|
-
# cb_record_failure "hook-name"
|
|
29
|
-
# fi
|
|
30
|
-
# else
|
|
31
|
-
# # Circuit is open, return safe default
|
|
32
|
-
# fi
|
|
33
|
-
#
|
|
34
|
-
# v1.0.0 - Initial implementation (2025-12-17)
|
|
35
|
-
|
|
36
|
-
set -o pipefail
|
|
37
|
-
|
|
38
|
-
# === Configuration ===
|
|
39
|
-
CB_STATE_DIR="${SPECWEAVE_STATE_DIR:-.specweave/state}/circuit-breakers"
|
|
40
|
-
CB_FAILURE_THRESHOLD="${CB_FAILURE_THRESHOLD:-5}" # Failures before OPEN
|
|
41
|
-
CB_FAILURE_WINDOW_SEC="${CB_FAILURE_WINDOW_SEC:-60}" # Sliding window for counting failures
|
|
42
|
-
CB_RECOVERY_TIMEOUT_SEC="${CB_RECOVERY_TIMEOUT_SEC:-30}" # Time in OPEN before HALF-OPEN
|
|
43
|
-
CB_SUCCESS_THRESHOLD="${CB_SUCCESS_THRESHOLD:-3}" # Successes in HALF-OPEN to CLOSE
|
|
44
|
-
CB_DEBUG="${CB_DEBUG:-0}"
|
|
45
|
-
|
|
46
|
-
# States
|
|
47
|
-
CB_STATE_CLOSED="CLOSED"
|
|
48
|
-
CB_STATE_OPEN="OPEN"
|
|
49
|
-
CB_STATE_HALF_OPEN="HALF_OPEN"
|
|
50
|
-
|
|
51
|
-
# === Initialization ===
|
|
52
|
-
_cb_init() {
|
|
53
|
-
mkdir -p "$CB_STATE_DIR" 2>/dev/null || true
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
# === Logging ===
|
|
57
|
-
_cb_log() {
|
|
58
|
-
[[ "$CB_DEBUG" == "1" ]] && echo "[CB $(date +%H:%M:%S)] $*" >&2
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
# === State File Paths ===
|
|
62
|
-
_cb_state_file() {
|
|
63
|
-
local name="$1"
|
|
64
|
-
echo "$CB_STATE_DIR/${name}.state"
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
_cb_failures_file() {
|
|
68
|
-
local name="$1"
|
|
69
|
-
echo "$CB_STATE_DIR/${name}.failures"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
_cb_successes_file() {
|
|
73
|
-
local name="$1"
|
|
74
|
-
echo "$CB_STATE_DIR/${name}.successes"
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
# === Read/Write State ===
|
|
78
|
-
_cb_read_state() {
|
|
79
|
-
local name="$1"
|
|
80
|
-
local state_file
|
|
81
|
-
state_file=$(_cb_state_file "$name")
|
|
82
|
-
|
|
83
|
-
if [[ -f "$state_file" ]]; then
|
|
84
|
-
cat "$state_file" 2>/dev/null || echo "$CB_STATE_CLOSED"
|
|
85
|
-
else
|
|
86
|
-
echo "$CB_STATE_CLOSED"
|
|
87
|
-
fi
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
_cb_write_state() {
|
|
91
|
-
local name="$1"
|
|
92
|
-
local state="$2"
|
|
93
|
-
local state_file
|
|
94
|
-
state_file=$(_cb_state_file "$name")
|
|
95
|
-
|
|
96
|
-
_cb_init
|
|
97
|
-
echo "$state" > "$state_file" 2>/dev/null || true
|
|
98
|
-
_cb_log "State transition for $name: -> $state"
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
# === Timestamp helpers ===
|
|
102
|
-
_cb_now() {
|
|
103
|
-
date +%s
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
_cb_file_mtime() {
|
|
107
|
-
local file="$1"
|
|
108
|
-
if [[ "$(uname)" == "Darwin" ]]; then
|
|
109
|
-
stat -f %m "$file" 2>/dev/null || echo "0"
|
|
110
|
-
else
|
|
111
|
-
stat -c %Y "$file" 2>/dev/null || echo "0"
|
|
112
|
-
fi
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
# === Failure Tracking (Sliding Window) ===
|
|
116
|
-
_cb_count_recent_failures() {
|
|
117
|
-
local name="$1"
|
|
118
|
-
local failures_file
|
|
119
|
-
failures_file=$(_cb_failures_file "$name")
|
|
120
|
-
|
|
121
|
-
[[ ! -f "$failures_file" ]] && echo "0" && return
|
|
122
|
-
|
|
123
|
-
local now
|
|
124
|
-
now=$(_cb_now)
|
|
125
|
-
local window_start=$((now - CB_FAILURE_WINDOW_SEC))
|
|
126
|
-
local count=0
|
|
127
|
-
|
|
128
|
-
# Read failure timestamps and count those within window
|
|
129
|
-
while IFS= read -r timestamp; do
|
|
130
|
-
[[ -z "$timestamp" ]] && continue
|
|
131
|
-
if [[ "$timestamp" -ge "$window_start" ]]; then
|
|
132
|
-
count=$((count + 1))
|
|
133
|
-
fi
|
|
134
|
-
done < "$failures_file"
|
|
135
|
-
|
|
136
|
-
echo "$count"
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
_cb_add_failure() {
|
|
140
|
-
local name="$1"
|
|
141
|
-
local failures_file
|
|
142
|
-
failures_file=$(_cb_failures_file "$name")
|
|
143
|
-
|
|
144
|
-
_cb_init
|
|
145
|
-
local now
|
|
146
|
-
now=$(_cb_now)
|
|
147
|
-
|
|
148
|
-
# Append timestamp
|
|
149
|
-
echo "$now" >> "$failures_file" 2>/dev/null || true
|
|
150
|
-
|
|
151
|
-
# Cleanup old entries (keep only last 100)
|
|
152
|
-
if [[ -f "$failures_file" ]]; then
|
|
153
|
-
tail -100 "$failures_file" > "${failures_file}.tmp" 2>/dev/null && \
|
|
154
|
-
mv "${failures_file}.tmp" "$failures_file" 2>/dev/null || true
|
|
155
|
-
fi
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
_cb_clear_failures() {
|
|
159
|
-
local name="$1"
|
|
160
|
-
local failures_file
|
|
161
|
-
failures_file=$(_cb_failures_file "$name")
|
|
162
|
-
rm -f "$failures_file" 2>/dev/null || true
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
# === Success Tracking (Half-Open State) ===
|
|
166
|
-
_cb_count_successes() {
|
|
167
|
-
local name="$1"
|
|
168
|
-
local successes_file
|
|
169
|
-
successes_file=$(_cb_successes_file "$name")
|
|
170
|
-
|
|
171
|
-
[[ ! -f "$successes_file" ]] && echo "0" && return
|
|
172
|
-
|
|
173
|
-
wc -l < "$successes_file" 2>/dev/null | tr -d ' '
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
_cb_add_success() {
|
|
177
|
-
local name="$1"
|
|
178
|
-
local successes_file
|
|
179
|
-
successes_file=$(_cb_successes_file "$name")
|
|
180
|
-
|
|
181
|
-
_cb_init
|
|
182
|
-
echo "$(_cb_now)" >> "$successes_file" 2>/dev/null || true
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
_cb_clear_successes() {
|
|
186
|
-
local name="$1"
|
|
187
|
-
local successes_file
|
|
188
|
-
successes_file=$(_cb_successes_file "$name")
|
|
189
|
-
rm -f "$successes_file" 2>/dev/null || true
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
# === State Transitions ===
|
|
193
|
-
_cb_check_should_open() {
|
|
194
|
-
local name="$1"
|
|
195
|
-
local failure_count
|
|
196
|
-
failure_count=$(_cb_count_recent_failures "$name")
|
|
197
|
-
|
|
198
|
-
if [[ "$failure_count" -ge "$CB_FAILURE_THRESHOLD" ]]; then
|
|
199
|
-
_cb_log "$name: Failure threshold reached ($failure_count >= $CB_FAILURE_THRESHOLD)"
|
|
200
|
-
return 0 # Should open
|
|
201
|
-
fi
|
|
202
|
-
return 1 # Should not open
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
_cb_check_should_half_open() {
|
|
206
|
-
local name="$1"
|
|
207
|
-
local state_file
|
|
208
|
-
state_file=$(_cb_state_file "$name")
|
|
209
|
-
|
|
210
|
-
[[ ! -f "$state_file" ]] && return 1
|
|
211
|
-
|
|
212
|
-
local state_mtime
|
|
213
|
-
state_mtime=$(_cb_file_mtime "$state_file")
|
|
214
|
-
local now
|
|
215
|
-
now=$(_cb_now)
|
|
216
|
-
local age=$((now - state_mtime))
|
|
217
|
-
|
|
218
|
-
if [[ "$age" -ge "$CB_RECOVERY_TIMEOUT_SEC" ]]; then
|
|
219
|
-
_cb_log "$name: Recovery timeout reached (${age}s >= ${CB_RECOVERY_TIMEOUT_SEC}s)"
|
|
220
|
-
return 0 # Should transition to half-open
|
|
221
|
-
fi
|
|
222
|
-
return 1
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
_cb_check_should_close() {
|
|
226
|
-
local name="$1"
|
|
227
|
-
local success_count
|
|
228
|
-
success_count=$(_cb_count_successes "$name")
|
|
229
|
-
|
|
230
|
-
if [[ "$success_count" -ge "$CB_SUCCESS_THRESHOLD" ]]; then
|
|
231
|
-
_cb_log "$name: Success threshold reached ($success_count >= $CB_SUCCESS_THRESHOLD)"
|
|
232
|
-
return 0 # Should close
|
|
233
|
-
fi
|
|
234
|
-
return 1
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
# === Public API ===
|
|
238
|
-
|
|
239
|
-
# Check if request should be allowed
|
|
240
|
-
# Returns 0 if allowed, 1 if circuit is open
|
|
241
|
-
cb_allow_request() {
|
|
242
|
-
local name="${1:-default}"
|
|
243
|
-
|
|
244
|
-
_cb_init
|
|
245
|
-
|
|
246
|
-
local state
|
|
247
|
-
state=$(_cb_read_state "$name")
|
|
248
|
-
|
|
249
|
-
case "$state" in
|
|
250
|
-
"$CB_STATE_CLOSED")
|
|
251
|
-
_cb_log "$name: CLOSED - allowing request"
|
|
252
|
-
return 0
|
|
253
|
-
;;
|
|
254
|
-
|
|
255
|
-
"$CB_STATE_OPEN")
|
|
256
|
-
# Check if we should transition to half-open
|
|
257
|
-
if _cb_check_should_half_open "$name"; then
|
|
258
|
-
_cb_write_state "$name" "$CB_STATE_HALF_OPEN"
|
|
259
|
-
_cb_clear_successes "$name"
|
|
260
|
-
_cb_log "$name: OPEN -> HALF_OPEN - allowing test request"
|
|
261
|
-
return 0
|
|
262
|
-
fi
|
|
263
|
-
_cb_log "$name: OPEN - rejecting request (fail-fast)"
|
|
264
|
-
return 1
|
|
265
|
-
;;
|
|
266
|
-
|
|
267
|
-
"$CB_STATE_HALF_OPEN")
|
|
268
|
-
_cb_log "$name: HALF_OPEN - allowing test request"
|
|
269
|
-
return 0
|
|
270
|
-
;;
|
|
271
|
-
|
|
272
|
-
*)
|
|
273
|
-
# Unknown state, default to closed
|
|
274
|
-
_cb_write_state "$name" "$CB_STATE_CLOSED"
|
|
275
|
-
return 0
|
|
276
|
-
;;
|
|
277
|
-
esac
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
# Record successful request
|
|
281
|
-
cb_record_success() {
|
|
282
|
-
local name="${1:-default}"
|
|
283
|
-
|
|
284
|
-
local state
|
|
285
|
-
state=$(_cb_read_state "$name")
|
|
286
|
-
|
|
287
|
-
case "$state" in
|
|
288
|
-
"$CB_STATE_CLOSED")
|
|
289
|
-
# Clear any old failures on success
|
|
290
|
-
# (helps prevent lingering failures from keeping count high)
|
|
291
|
-
;;
|
|
292
|
-
|
|
293
|
-
"$CB_STATE_HALF_OPEN")
|
|
294
|
-
_cb_add_success "$name"
|
|
295
|
-
if _cb_check_should_close "$name"; then
|
|
296
|
-
_cb_write_state "$name" "$CB_STATE_CLOSED"
|
|
297
|
-
_cb_clear_failures "$name"
|
|
298
|
-
_cb_clear_successes "$name"
|
|
299
|
-
_cb_log "$name: HALF_OPEN -> CLOSED (recovered)"
|
|
300
|
-
fi
|
|
301
|
-
;;
|
|
302
|
-
esac
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
# Record failed request
|
|
306
|
-
cb_record_failure() {
|
|
307
|
-
local name="${1:-default}"
|
|
308
|
-
|
|
309
|
-
local state
|
|
310
|
-
state=$(_cb_read_state "$name")
|
|
311
|
-
|
|
312
|
-
_cb_add_failure "$name"
|
|
313
|
-
|
|
314
|
-
case "$state" in
|
|
315
|
-
"$CB_STATE_CLOSED")
|
|
316
|
-
if _cb_check_should_open "$name"; then
|
|
317
|
-
_cb_write_state "$name" "$CB_STATE_OPEN"
|
|
318
|
-
_cb_log "$name: CLOSED -> OPEN (too many failures)"
|
|
319
|
-
fi
|
|
320
|
-
;;
|
|
321
|
-
|
|
322
|
-
"$CB_STATE_HALF_OPEN")
|
|
323
|
-
# Any failure in half-open immediately opens circuit
|
|
324
|
-
_cb_write_state "$name" "$CB_STATE_OPEN"
|
|
325
|
-
_cb_clear_successes "$name"
|
|
326
|
-
_cb_log "$name: HALF_OPEN -> OPEN (failed during recovery)"
|
|
327
|
-
;;
|
|
328
|
-
esac
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
# Get circuit breaker status
|
|
332
|
-
cb_get_status() {
|
|
333
|
-
local name="${1:-default}"
|
|
334
|
-
|
|
335
|
-
_cb_init
|
|
336
|
-
|
|
337
|
-
local state
|
|
338
|
-
state=$(_cb_read_state "$name")
|
|
339
|
-
local failures
|
|
340
|
-
failures=$(_cb_count_recent_failures "$name")
|
|
341
|
-
local successes
|
|
342
|
-
successes=$(_cb_count_successes "$name")
|
|
343
|
-
|
|
344
|
-
echo "{\"name\":\"$name\",\"state\":\"$state\",\"failures\":$failures,\"successes\":$successes,\"failure_threshold\":$CB_FAILURE_THRESHOLD,\"recovery_timeout_sec\":$CB_RECOVERY_TIMEOUT_SEC}"
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
# Force reset circuit breaker
|
|
348
|
-
cb_reset() {
|
|
349
|
-
local name="${1:-default}"
|
|
350
|
-
|
|
351
|
-
_cb_write_state "$name" "$CB_STATE_CLOSED"
|
|
352
|
-
_cb_clear_failures "$name"
|
|
353
|
-
_cb_clear_successes "$name"
|
|
354
|
-
_cb_log "$name: Force reset to CLOSED"
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
# List all circuit breakers
|
|
358
|
-
cb_list_all() {
|
|
359
|
-
_cb_init
|
|
360
|
-
|
|
361
|
-
local result="["
|
|
362
|
-
local first=true
|
|
363
|
-
|
|
364
|
-
for state_file in "$CB_STATE_DIR"/*.state; do
|
|
365
|
-
[[ ! -f "$state_file" ]] && continue
|
|
366
|
-
|
|
367
|
-
local name
|
|
368
|
-
name=$(basename "$state_file" .state)
|
|
369
|
-
|
|
370
|
-
if [[ "$first" == "true" ]]; then
|
|
371
|
-
first=false
|
|
372
|
-
else
|
|
373
|
-
result="$result,"
|
|
374
|
-
fi
|
|
375
|
-
|
|
376
|
-
result="$result$(cb_get_status "$name")"
|
|
377
|
-
done
|
|
378
|
-
|
|
379
|
-
result="$result]"
|
|
380
|
-
echo "$result"
|
|
381
|
-
}
|