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.
Files changed (123) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CLAUDE.md +205 -148
  3. package/README.md +0 -2
  4. package/bin/specweave.js +11 -0
  5. package/dist/src/cli/commands/init.js +1 -1
  6. package/dist/src/cli/commands/init.js.map +1 -1
  7. package/dist/src/cli/commands/update-instructions.d.ts +16 -0
  8. package/dist/src/cli/commands/update-instructions.d.ts.map +1 -0
  9. package/dist/src/cli/commands/update-instructions.js +134 -0
  10. package/dist/src/cli/commands/update-instructions.js.map +1 -0
  11. package/dist/src/cli/helpers/init/directory-structure.d.ts +28 -1
  12. package/dist/src/cli/helpers/init/directory-structure.d.ts.map +1 -1
  13. package/dist/src/cli/helpers/init/directory-structure.js +163 -33
  14. package/dist/src/cli/helpers/init/directory-structure.js.map +1 -1
  15. package/dist/src/cli/helpers/init/index.d.ts +2 -1
  16. package/dist/src/cli/helpers/init/index.d.ts.map +1 -1
  17. package/dist/src/cli/helpers/init/index.js +3 -1
  18. package/dist/src/cli/helpers/init/index.js.map +1 -1
  19. package/dist/src/cli/helpers/init/instruction-file-merger.d.ts +23 -0
  20. package/dist/src/cli/helpers/init/instruction-file-merger.d.ts.map +1 -0
  21. package/dist/src/cli/helpers/init/instruction-file-merger.js +243 -0
  22. package/dist/src/cli/helpers/init/instruction-file-merger.js.map +1 -0
  23. package/dist/src/cli/helpers/init/plugin-installer.js +49 -0
  24. package/dist/src/cli/helpers/init/plugin-installer.js.map +1 -1
  25. package/dist/src/config/types.d.ts +2 -2
  26. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts +26 -0
  27. package/dist/src/core/living-docs/external-sync-orchestrator.d.ts.map +1 -1
  28. package/dist/src/core/living-docs/external-sync-orchestrator.js +61 -0
  29. package/dist/src/core/living-docs/external-sync-orchestrator.js.map +1 -1
  30. package/dist/src/core/living-docs/scaffolding/index.d.ts +12 -0
  31. package/dist/src/core/living-docs/scaffolding/index.d.ts.map +1 -0
  32. package/dist/src/core/living-docs/scaffolding/index.js +15 -0
  33. package/dist/src/core/living-docs/scaffolding/index.js.map +1 -0
  34. package/dist/src/core/living-docs/scaffolding/merger.d.ts +183 -0
  35. package/dist/src/core/living-docs/scaffolding/merger.d.ts.map +1 -0
  36. package/dist/src/core/living-docs/scaffolding/merger.js +523 -0
  37. package/dist/src/core/living-docs/scaffolding/merger.js.map +1 -0
  38. package/dist/src/core/living-docs/scaffolding/scaffold.d.ts +102 -0
  39. package/dist/src/core/living-docs/scaffolding/scaffold.d.ts.map +1 -0
  40. package/dist/src/core/living-docs/scaffolding/scaffold.js +346 -0
  41. package/dist/src/core/living-docs/scaffolding/scaffold.js.map +1 -0
  42. package/dist/src/core/living-docs/scaffolding/template-engine.d.ts +108 -0
  43. package/dist/src/core/living-docs/scaffolding/template-engine.d.ts.map +1 -0
  44. package/dist/src/core/living-docs/scaffolding/template-engine.js +204 -0
  45. package/dist/src/core/living-docs/scaffolding/template-engine.js.map +1 -0
  46. package/dist/src/core/living-docs/sync-helpers/generators.d.ts +38 -2
  47. package/dist/src/core/living-docs/sync-helpers/generators.d.ts.map +1 -1
  48. package/dist/src/core/living-docs/sync-helpers/generators.js +65 -10
  49. package/dist/src/core/living-docs/sync-helpers/generators.js.map +1 -1
  50. package/dist/src/core/living-docs/sync-helpers/index.d.ts +1 -1
  51. package/dist/src/core/living-docs/sync-helpers/index.d.ts.map +1 -1
  52. package/dist/src/core/living-docs/sync-helpers/index.js.map +1 -1
  53. package/dist/src/core/tools/index.d.ts +11 -0
  54. package/dist/src/core/tools/index.d.ts.map +1 -0
  55. package/dist/src/core/tools/index.js +10 -0
  56. package/dist/src/core/tools/index.js.map +1 -0
  57. package/dist/src/core/tools/tool-event-bus.d.ts +33 -0
  58. package/dist/src/core/tools/tool-event-bus.d.ts.map +1 -0
  59. package/dist/src/core/tools/tool-event-bus.js +84 -0
  60. package/dist/src/core/tools/tool-event-bus.js.map +1 -0
  61. package/dist/src/core/tools/tool-index-builder.d.ts +27 -0
  62. package/dist/src/core/tools/tool-index-builder.d.ts.map +1 -0
  63. package/dist/src/core/tools/tool-index-builder.js +289 -0
  64. package/dist/src/core/tools/tool-index-builder.js.map +1 -0
  65. package/dist/src/core/tools/tool-registry.d.ts +51 -0
  66. package/dist/src/core/tools/tool-registry.d.ts.map +1 -0
  67. package/dist/src/core/tools/tool-registry.js +224 -0
  68. package/dist/src/core/tools/tool-registry.js.map +1 -0
  69. package/dist/src/core/tools/tool-search-engine.d.ts +22 -0
  70. package/dist/src/core/tools/tool-search-engine.d.ts.map +1 -0
  71. package/dist/src/core/tools/tool-search-engine.js +174 -0
  72. package/dist/src/core/tools/tool-search-engine.js.map +1 -0
  73. package/dist/src/core/tools/types/tool-registry-types.d.ts +112 -0
  74. package/dist/src/core/tools/types/tool-registry-types.d.ts.map +1 -0
  75. package/dist/src/core/tools/types/tool-registry-types.js +7 -0
  76. package/dist/src/core/tools/types/tool-registry-types.js.map +1 -0
  77. package/dist/src/init/compliance/types.d.ts +1 -1
  78. package/package.json +1 -1
  79. package/plugins/specweave/hooks/hooks.json +3 -13
  80. package/plugins/specweave/hooks/lib/common-setup.sh +47 -321
  81. package/plugins/specweave/hooks/lib/migrate-increment-work.sh +5 -5
  82. package/plugins/specweave/hooks/lib/sync-spec-content.sh +5 -5
  83. package/plugins/specweave/hooks/universal/dispatcher.mjs +4 -5
  84. package/plugins/specweave/hooks/universal/fail-fast-wrapper.sh +43 -296
  85. package/plugins/specweave/hooks/universal/hook-wrapper.sh +3 -1
  86. package/plugins/specweave/hooks/user-prompt-submit.sh +1 -1
  87. package/plugins/specweave/hooks/v2/dispatchers/post-tool-use.sh +2 -2
  88. package/plugins/specweave/hooks/v2/dispatchers/session-start.sh +1 -10
  89. package/plugins/specweave/hooks/v2/guards/completion-guard.sh +12 -29
  90. package/plugins/specweave/hooks/v2/guards/increment-duplicate-guard.sh +27 -29
  91. package/plugins/specweave/hooks/v2/guards/metadata-json-guard.sh +10 -4
  92. package/plugins/specweave/hooks/v2/guards/spec-validation-guard.sh +139 -0
  93. package/plugins/specweave/hooks/v2/guards/task-ac-sync-guard.sh +4 -2
  94. package/plugins/specweave/hooks/v2/session-end.sh +3 -1
  95. package/plugins/specweave/hooks/v2/session-start.sh +3 -1
  96. package/plugins/specweave/skills/increment-planner/templates/plan.md +14 -0
  97. package/plugins/specweave/skills/update-instructions/SKILL.md +80 -0
  98. package/plugins/specweave-ado/hooks/post-living-docs-update.sh +1 -1
  99. package/plugins/specweave-mobile/README.md +55 -35
  100. package/plugins/specweave-mobile/agents/mobile-architect/AGENT.md +805 -329
  101. package/plugins/specweave-mobile/skills/expo-workflow/SKILL.md +226 -9
  102. package/plugins/specweave-mobile/skills/native-modules/SKILL.md +221 -20
  103. package/plugins/specweave-mobile/skills/performance-optimization/SKILL.md +186 -14
  104. package/plugins/specweave-mobile/skills/react-native-setup/SKILL.md +151 -54
  105. package/plugins/specweave-release/commands/npm.md +61 -17
  106. package/plugins/specweave-release/hooks/post-task-completion.sh +2 -3
  107. package/src/templates/AGENTS.md.template +34 -0
  108. package/src/templates/CLAUDE.md.template +121 -155
  109. package/plugins/specweave/hooks/config-env-separator.sh +0 -99
  110. package/plugins/specweave/hooks/github-metadata-guard.sh +0 -73
  111. package/plugins/specweave/hooks/lib/circuit-breaker.sh +0 -381
  112. package/plugins/specweave/hooks/lib/crash-prevention.sh +0 -336
  113. package/plugins/specweave/hooks/lib/logging.sh +0 -231
  114. package/plugins/specweave/hooks/lib/metrics.sh +0 -347
  115. package/plugins/specweave/hooks/lib/semaphore.sh +0 -216
  116. package/plugins/specweave/hooks/project-folder-guard.sh +0 -274
  117. package/plugins/specweave/hooks/spec-project-validator.sh +0 -210
  118. package/plugins/specweave/hooks/v2/guards/bash-file-guard.sh +0 -212
  119. package/plugins/specweave/hooks/v2/guards/bash-file-guard.test.sh +0 -163
  120. package/plugins/specweave/hooks/v2/guards/features-folder-guard.sh +0 -51
  121. package/plugins/specweave/hooks/v2/guards/increment-root-guard.sh +0 -63
  122. package/plugins/specweave/hooks/v2/guards/per-us-project-validator.sh +0 -335
  123. 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
- }