codeharness 0.6.1
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/bin/codeharness +9 -0
- package/dist/chunk-7ZD2ZNDU.js +540 -0
- package/dist/docker-CT57JGM7.js +33 -0
- package/dist/index.js +6104 -0
- package/package.json +39 -0
- package/ralph/AGENTS.md +38 -0
- package/ralph/bridge.sh +421 -0
- package/ralph/db_schema_gen.sh +109 -0
- package/ralph/doc_gardener.sh +352 -0
- package/ralph/drivers/claude-code.sh +160 -0
- package/ralph/exec_plans.sh +252 -0
- package/ralph/harness_status.sh +156 -0
- package/ralph/lib/circuit_breaker.sh +210 -0
- package/ralph/lib/date_utils.sh +60 -0
- package/ralph/lib/timeout_utils.sh +77 -0
- package/ralph/onboard.sh +83 -0
- package/ralph/ralph.sh +1006 -0
- package/ralph/retro.sh +298 -0
- package/ralph/validate_epic_docs.sh +129 -0
- package/ralph/verify_gates.sh +241 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# exec_plans.sh — Exec-Plan Lifecycle Management
|
|
3
|
+
# Generates per-story exec-plan files and moves them from active→completed.
|
|
4
|
+
#
|
|
5
|
+
# Usage:
|
|
6
|
+
# ralph/exec_plans.sh generate --progress PATH --output-dir DIR
|
|
7
|
+
# ralph/exec_plans.sh complete --story-id ID --output-dir DIR --proof-path PATH
|
|
8
|
+
|
|
9
|
+
set -e
|
|
10
|
+
|
|
11
|
+
# Portable in-place sed (macOS uses -i '', Linux uses -i)
|
|
12
|
+
sed_i() {
|
|
13
|
+
if sed --version 2>/dev/null | grep -q GNU; then
|
|
14
|
+
sed -i "$@"
|
|
15
|
+
else
|
|
16
|
+
sed -i '' "$@"
|
|
17
|
+
fi
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# ─── CLI ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
ACTION=""
|
|
23
|
+
PROGRESS_FILE=""
|
|
24
|
+
OUTPUT_DIR=""
|
|
25
|
+
STORY_ID=""
|
|
26
|
+
PROOF_PATH=""
|
|
27
|
+
|
|
28
|
+
show_help() {
|
|
29
|
+
cat << 'HELPEOF'
|
|
30
|
+
Exec-Plan Lifecycle — track stories from active to completed
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
ralph/exec_plans.sh generate --progress PATH --output-dir DIR
|
|
34
|
+
ralph/exec_plans.sh complete --story-id ID --output-dir DIR --proof-path PATH
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
generate Create exec-plan files from progress.json
|
|
38
|
+
Pending/in_progress → active/, complete → completed/
|
|
39
|
+
complete Move an exec-plan from active/ to completed/
|
|
40
|
+
Appends verification summary and Showboat proof link
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--progress PATH Path to ralph/progress.json
|
|
44
|
+
--output-dir DIR Base directory for exec-plans (contains active/ and completed/)
|
|
45
|
+
--story-id ID Story ID to complete (e.g., "1.1")
|
|
46
|
+
--proof-path PATH Relative path to Showboat proof document
|
|
47
|
+
-h, --help Show this help message
|
|
48
|
+
HELPEOF
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Parse action first
|
|
52
|
+
if [[ $# -gt 0 && "$1" != -* ]]; then
|
|
53
|
+
ACTION="$1"
|
|
54
|
+
shift
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
while [[ $# -gt 0 ]]; do
|
|
58
|
+
case $1 in
|
|
59
|
+
-h|--help)
|
|
60
|
+
show_help
|
|
61
|
+
exit 0
|
|
62
|
+
;;
|
|
63
|
+
--progress)
|
|
64
|
+
PROGRESS_FILE="$2"
|
|
65
|
+
shift 2
|
|
66
|
+
;;
|
|
67
|
+
--output-dir)
|
|
68
|
+
OUTPUT_DIR="$2"
|
|
69
|
+
shift 2
|
|
70
|
+
;;
|
|
71
|
+
--story-id)
|
|
72
|
+
STORY_ID="$2"
|
|
73
|
+
shift 2
|
|
74
|
+
;;
|
|
75
|
+
--proof-path)
|
|
76
|
+
PROOF_PATH="$2"
|
|
77
|
+
shift 2
|
|
78
|
+
;;
|
|
79
|
+
*)
|
|
80
|
+
echo "Unknown option: $1" >&2
|
|
81
|
+
exit 1
|
|
82
|
+
;;
|
|
83
|
+
esac
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
# ─── Generate exec-plan for a single task ─────────────────────────────────
|
|
87
|
+
|
|
88
|
+
generate_exec_plan_from_index() {
|
|
89
|
+
local progress_file="$1"
|
|
90
|
+
local index="$2"
|
|
91
|
+
local target_dir="$3"
|
|
92
|
+
|
|
93
|
+
local id title epic description status proof_path
|
|
94
|
+
id=$(jq -r ".tasks[$index].id" "$progress_file")
|
|
95
|
+
title=$(jq -r ".tasks[$index].title" "$progress_file")
|
|
96
|
+
epic=$(jq -r ".tasks[$index].epic // \"Unknown\"" "$progress_file")
|
|
97
|
+
description=$(jq -r ".tasks[$index].description // \"\"" "$progress_file")
|
|
98
|
+
status=$(jq -r ".tasks[$index].status // \"pending\"" "$progress_file")
|
|
99
|
+
proof_path=$(jq -r ".tasks[$index].verification.proof_path // \"\"" "$progress_file")
|
|
100
|
+
|
|
101
|
+
local file="$target_dir/$id.md"
|
|
102
|
+
|
|
103
|
+
# Don't overwrite existing exec-plans (idempotency)
|
|
104
|
+
if [[ -f "$file" ]]; then
|
|
105
|
+
return 0
|
|
106
|
+
fi
|
|
107
|
+
|
|
108
|
+
# Build acceptance criteria list
|
|
109
|
+
local ac_list=""
|
|
110
|
+
local ac_count
|
|
111
|
+
ac_count=$(jq ".tasks[$index].acceptance_criteria | length" "$progress_file")
|
|
112
|
+
local j
|
|
113
|
+
for ((j=0; j<ac_count; j++)); do
|
|
114
|
+
local ac
|
|
115
|
+
ac=$(jq -r ".tasks[$index].acceptance_criteria[$j]" "$progress_file")
|
|
116
|
+
ac_list="${ac_list}- ${ac}
|
|
117
|
+
"
|
|
118
|
+
done
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
echo "# Story $id: $title"
|
|
122
|
+
echo ""
|
|
123
|
+
echo "**Epic:** $epic"
|
|
124
|
+
echo "**Status:** $status"
|
|
125
|
+
echo ""
|
|
126
|
+
echo "## Description"
|
|
127
|
+
echo ""
|
|
128
|
+
echo "$description"
|
|
129
|
+
echo ""
|
|
130
|
+
echo "## Acceptance Criteria"
|
|
131
|
+
echo ""
|
|
132
|
+
if [[ -n "$ac_list" ]]; then
|
|
133
|
+
echo "$ac_list"
|
|
134
|
+
else
|
|
135
|
+
echo "_No acceptance criteria defined._"
|
|
136
|
+
echo ""
|
|
137
|
+
fi
|
|
138
|
+
echo "## Progress Log"
|
|
139
|
+
echo ""
|
|
140
|
+
echo "_No entries yet._"
|
|
141
|
+
echo ""
|
|
142
|
+
echo "## Verification Status"
|
|
143
|
+
echo ""
|
|
144
|
+
echo "- **Proof document:** ${proof_path:-N/A}"
|
|
145
|
+
echo "- **Verified:** No"
|
|
146
|
+
echo "- **Iterations:** 0"
|
|
147
|
+
} > "$file"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# ─── Generate command ─────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
do_generate() {
|
|
153
|
+
if [[ -z "$PROGRESS_FILE" ]]; then
|
|
154
|
+
echo "Error: --progress is required" >&2
|
|
155
|
+
exit 1
|
|
156
|
+
fi
|
|
157
|
+
if [[ ! -f "$PROGRESS_FILE" ]]; then
|
|
158
|
+
echo "Error: progress file not found: $PROGRESS_FILE" >&2
|
|
159
|
+
exit 1
|
|
160
|
+
fi
|
|
161
|
+
if [[ -z "$OUTPUT_DIR" ]]; then
|
|
162
|
+
echo "Error: --output-dir is required" >&2
|
|
163
|
+
exit 1
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
mkdir -p "$OUTPUT_DIR/active" "$OUTPUT_DIR/completed"
|
|
167
|
+
|
|
168
|
+
local task_count
|
|
169
|
+
task_count=$(jq '.tasks | length' "$PROGRESS_FILE")
|
|
170
|
+
|
|
171
|
+
for ((i=0; i<task_count; i++)); do
|
|
172
|
+
local status
|
|
173
|
+
status=$(jq -r ".tasks[$i].status" "$PROGRESS_FILE")
|
|
174
|
+
|
|
175
|
+
if [[ "$status" == "complete" ]]; then
|
|
176
|
+
generate_exec_plan_from_index "$PROGRESS_FILE" "$i" "$OUTPUT_DIR/completed"
|
|
177
|
+
else
|
|
178
|
+
generate_exec_plan_from_index "$PROGRESS_FILE" "$i" "$OUTPUT_DIR/active"
|
|
179
|
+
fi
|
|
180
|
+
done
|
|
181
|
+
|
|
182
|
+
local active_count completed_count
|
|
183
|
+
active_count=$(find "$OUTPUT_DIR/active" -maxdepth 1 -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
184
|
+
completed_count=$(find "$OUTPUT_DIR/completed" -maxdepth 1 -name "*.md" 2>/dev/null | wc -l | tr -d ' ')
|
|
185
|
+
echo "[OK] Exec-plans: $active_count active, $completed_count completed"
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ─── Complete command ─────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
do_complete() {
|
|
191
|
+
if [[ -z "$STORY_ID" ]]; then
|
|
192
|
+
echo "Error: --story-id is required" >&2
|
|
193
|
+
exit 1
|
|
194
|
+
fi
|
|
195
|
+
if [[ -z "$OUTPUT_DIR" ]]; then
|
|
196
|
+
echo "Error: --output-dir is required" >&2
|
|
197
|
+
exit 1
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
local active_file="$OUTPUT_DIR/active/$STORY_ID.md"
|
|
201
|
+
local completed_file="$OUTPUT_DIR/completed/$STORY_ID.md"
|
|
202
|
+
|
|
203
|
+
if [[ ! -f "$active_file" ]]; then
|
|
204
|
+
echo "Error: exec-plan not found: $active_file" >&2
|
|
205
|
+
exit 1
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
mkdir -p "$OUTPUT_DIR/completed"
|
|
209
|
+
|
|
210
|
+
# Append verification summary
|
|
211
|
+
local timestamp
|
|
212
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
213
|
+
|
|
214
|
+
cat >> "$active_file" << COMPLETION
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Verified
|
|
219
|
+
|
|
220
|
+
- **Completed at:** $timestamp
|
|
221
|
+
- **Showboat proof:** [${PROOF_PATH}](${PROOF_PATH})
|
|
222
|
+
- **Result:** All verification gates passed
|
|
223
|
+
COMPLETION
|
|
224
|
+
|
|
225
|
+
# Update status in the file
|
|
226
|
+
sed_i "s/\*\*Status:\*\* .*/\*\*Status:\*\* complete/" "$active_file"
|
|
227
|
+
sed_i "s/\*\*Verified:\*\* No/\*\*Verified:\*\* Yes/" "$active_file"
|
|
228
|
+
|
|
229
|
+
# Move to completed
|
|
230
|
+
mv "$active_file" "$completed_file"
|
|
231
|
+
echo "[OK] Exec-plan $STORY_ID moved to completed/"
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
# ─── Main ─────────────────────────────────────────────────────────────────
|
|
235
|
+
|
|
236
|
+
case "$ACTION" in
|
|
237
|
+
generate)
|
|
238
|
+
do_generate
|
|
239
|
+
;;
|
|
240
|
+
complete)
|
|
241
|
+
do_complete
|
|
242
|
+
;;
|
|
243
|
+
"")
|
|
244
|
+
echo "Error: specify a command: generate or complete" >&2
|
|
245
|
+
show_help
|
|
246
|
+
exit 1
|
|
247
|
+
;;
|
|
248
|
+
*)
|
|
249
|
+
echo "Unknown command: $ACTION" >&2
|
|
250
|
+
exit 1
|
|
251
|
+
;;
|
|
252
|
+
esac
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# harness_status.sh — Show harness health, sprint progress, and verification state
|
|
3
|
+
# Follows the `git status` model: health → enforcement → progress → next action
|
|
4
|
+
#
|
|
5
|
+
# Usage: ralph/harness_status.sh --project-dir DIR
|
|
6
|
+
|
|
7
|
+
set -e
|
|
8
|
+
|
|
9
|
+
PROJECT_DIR=""
|
|
10
|
+
|
|
11
|
+
show_help() {
|
|
12
|
+
cat << 'HELPEOF'
|
|
13
|
+
Harness Status — show health, progress, and verification at a glance
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
ralph/harness_status.sh --project-dir DIR
|
|
17
|
+
|
|
18
|
+
Output sections:
|
|
19
|
+
1. Health line (version, stack, Docker, Victoria)
|
|
20
|
+
2. Enforcement config (frontend, database, api, observability)
|
|
21
|
+
3. Sprint progress (per-story pass/pending/in-progress)
|
|
22
|
+
4. Next action hint
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--project-dir DIR Project root directory
|
|
26
|
+
-h, --help Show this help message
|
|
27
|
+
HELPEOF
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
while [[ $# -gt 0 ]]; do
|
|
31
|
+
case $1 in
|
|
32
|
+
-h|--help)
|
|
33
|
+
show_help
|
|
34
|
+
exit 0
|
|
35
|
+
;;
|
|
36
|
+
--project-dir)
|
|
37
|
+
PROJECT_DIR="$2"
|
|
38
|
+
shift 2
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
echo "Unknown option: $1" >&2
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
|
|
47
|
+
if [[ -z "$PROJECT_DIR" ]]; then
|
|
48
|
+
echo "Error: --project-dir is required" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
STATE_FILE="$PROJECT_DIR/.claude/codeharness.local.md"
|
|
53
|
+
PROGRESS_FILE="$PROJECT_DIR/ralph/progress.json"
|
|
54
|
+
LOOP_STATUS_FILE="$PROJECT_DIR/ralph/status.json"
|
|
55
|
+
|
|
56
|
+
# ─── Check initialized ───────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
if [[ ! -f "$STATE_FILE" ]]; then
|
|
59
|
+
echo "Harness not initialized."
|
|
60
|
+
echo ""
|
|
61
|
+
echo " → Run /harness-init to set up the harness"
|
|
62
|
+
exit 1
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# ─── Parse state file ────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
get_yaml_val() {
|
|
68
|
+
local key="$1"
|
|
69
|
+
grep -o "${key}: *[^ ]*" "$STATE_FILE" 2>/dev/null | head -1 | sed "s/${key}: *//" | tr -d '"'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
VERSION=$(get_yaml_val "harness_version")
|
|
73
|
+
STACK=$(get_yaml_val "stack")
|
|
74
|
+
|
|
75
|
+
# Enforcement
|
|
76
|
+
E_FRONTEND=$(get_yaml_val "frontend")
|
|
77
|
+
E_DATABASE=$(get_yaml_val "database")
|
|
78
|
+
E_API=$(get_yaml_val "api")
|
|
79
|
+
E_OBSERVABILITY=$(get_yaml_val "observability")
|
|
80
|
+
|
|
81
|
+
on_off() {
|
|
82
|
+
if [[ "$1" == "true" ]]; then echo "ON"; else echo "OFF"; fi
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# ─── Health line ──────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
echo "Harness Status — codeharness v${VERSION:-0.1.0}"
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
# Stack
|
|
91
|
+
echo " Stack: ${STACK:-unknown}"
|
|
92
|
+
|
|
93
|
+
# Loop status
|
|
94
|
+
if [[ -f "$LOOP_STATUS_FILE" ]]; then
|
|
95
|
+
local_loop=$(jq -r '.loop_count // 0' "$LOOP_STATUS_FILE" 2>/dev/null || echo "0")
|
|
96
|
+
local_status=$(jq -r '.status // "idle"' "$LOOP_STATUS_FILE" 2>/dev/null || echo "idle")
|
|
97
|
+
echo " Loop: iteration $local_loop ($local_status)"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
echo ""
|
|
101
|
+
|
|
102
|
+
# ─── Enforcement ──────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
echo " Enforcement:"
|
|
105
|
+
printf " frontend:%-4s database:%-4s api:%-4s observability:%-4s\n" \
|
|
106
|
+
"$(on_off "$E_FRONTEND")" "$(on_off "$E_DATABASE")" \
|
|
107
|
+
"$(on_off "$E_API")" "$(on_off "$E_OBSERVABILITY")"
|
|
108
|
+
echo ""
|
|
109
|
+
|
|
110
|
+
# ─── Sprint progress ─────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
113
|
+
total=$(jq '.tasks | length' "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
|
114
|
+
completed=$(jq '[.tasks[] | select(.status == "complete")] | length' "$PROGRESS_FILE" 2>/dev/null || echo "0")
|
|
115
|
+
|
|
116
|
+
echo " Sprint: $completed/$total stories"
|
|
117
|
+
echo ""
|
|
118
|
+
|
|
119
|
+
# Per-story status
|
|
120
|
+
jq -r '.tasks[] | "\(.status)\t\(.id)\t\(.title)"' "$PROGRESS_FILE" 2>/dev/null | \
|
|
121
|
+
while IFS=$'\t' read -r st id title; do
|
|
122
|
+
case "$st" in
|
|
123
|
+
complete) marker="[PASS]" ;;
|
|
124
|
+
in_progress) marker="[ >> ]" ;;
|
|
125
|
+
*) marker="[ ]" ;;
|
|
126
|
+
esac
|
|
127
|
+
# Truncate title to keep under 100 chars
|
|
128
|
+
printf " %s %s: %s\n" "$marker" "$id" "${title:0:60}"
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
echo ""
|
|
132
|
+
|
|
133
|
+
# Verification summary
|
|
134
|
+
VLOG="$PROJECT_DIR/ralph/verification-log.json"
|
|
135
|
+
if [[ -f "$VLOG" ]]; then
|
|
136
|
+
v_total=$(jq '.events | length' "$VLOG" 2>/dev/null || echo "0")
|
|
137
|
+
v_pass=$(jq '[.events[] | select(.result == "pass")] | length' "$VLOG" 2>/dev/null || echo "0")
|
|
138
|
+
echo " Verification: $v_pass passed / $v_total checks"
|
|
139
|
+
echo ""
|
|
140
|
+
fi
|
|
141
|
+
|
|
142
|
+
# Next action
|
|
143
|
+
current=$(jq -r '.tasks[] | select(.status == "pending" or .status == "in_progress") | .id' "$PROGRESS_FILE" 2>/dev/null | head -1)
|
|
144
|
+
if [[ -n "$current" && "$current" != "null" ]]; then
|
|
145
|
+
current_title=$(jq -r --arg id "$current" '.tasks[] | select(.id == $id) | .title' "$PROGRESS_FILE" 2>/dev/null)
|
|
146
|
+
echo " → Current: $current ($current_title)"
|
|
147
|
+
elif [[ "$completed" == "$total" && "$total" != "0" ]]; then
|
|
148
|
+
echo " → All stories complete!"
|
|
149
|
+
else
|
|
150
|
+
echo " → Run /harness-run to start execution"
|
|
151
|
+
fi
|
|
152
|
+
else
|
|
153
|
+
echo " No sprint progress file found."
|
|
154
|
+
echo ""
|
|
155
|
+
echo " → Run /harness-run to start execution"
|
|
156
|
+
fi
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# circuit_breaker.sh - Prevents runaway token consumption by detecting stagnation
|
|
3
|
+
# Vendored from snarktank/ralph, adapted for codeharness
|
|
4
|
+
# Based on Michael Nygard's "Release It!" pattern
|
|
5
|
+
|
|
6
|
+
source "$(dirname "${BASH_SOURCE[0]}")/date_utils.sh"
|
|
7
|
+
|
|
8
|
+
CB_STATE_CLOSED="CLOSED"
|
|
9
|
+
CB_STATE_HALF_OPEN="HALF_OPEN"
|
|
10
|
+
CB_STATE_OPEN="OPEN"
|
|
11
|
+
|
|
12
|
+
# Use HARNESS_STATE_DIR if set, otherwise default
|
|
13
|
+
HARNESS_STATE_DIR="${HARNESS_STATE_DIR:-.}"
|
|
14
|
+
CB_STATE_FILE="${HARNESS_STATE_DIR}/.circuit_breaker_state"
|
|
15
|
+
CB_HISTORY_FILE="${HARNESS_STATE_DIR}/.circuit_breaker_history"
|
|
16
|
+
|
|
17
|
+
CB_NO_PROGRESS_THRESHOLD=${CB_NO_PROGRESS_THRESHOLD:-3}
|
|
18
|
+
CB_SAME_ERROR_THRESHOLD=${CB_SAME_ERROR_THRESHOLD:-5}
|
|
19
|
+
CB_PERMISSION_DENIAL_THRESHOLD=${CB_PERMISSION_DENIAL_THRESHOLD:-2}
|
|
20
|
+
CB_COOLDOWN_MINUTES=${CB_COOLDOWN_MINUTES:-30}
|
|
21
|
+
CB_AUTO_RESET=${CB_AUTO_RESET:-false}
|
|
22
|
+
|
|
23
|
+
RED='\033[0;31m'
|
|
24
|
+
GREEN='\033[0;32m'
|
|
25
|
+
YELLOW='\033[1;33m'
|
|
26
|
+
NC='\033[0m'
|
|
27
|
+
|
|
28
|
+
init_circuit_breaker() {
|
|
29
|
+
if [[ -f "$CB_STATE_FILE" ]]; then
|
|
30
|
+
if ! jq '.' "$CB_STATE_FILE" > /dev/null 2>&1; then
|
|
31
|
+
rm -f "$CB_STATE_FILE"
|
|
32
|
+
fi
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [[ ! -f "$CB_STATE_FILE" ]]; then
|
|
36
|
+
jq -n \
|
|
37
|
+
--arg state "$CB_STATE_CLOSED" \
|
|
38
|
+
--arg last_change "$(get_iso_timestamp)" \
|
|
39
|
+
'{
|
|
40
|
+
state: $state,
|
|
41
|
+
last_change: $last_change,
|
|
42
|
+
consecutive_no_progress: 0,
|
|
43
|
+
consecutive_same_error: 0,
|
|
44
|
+
consecutive_permission_denials: 0,
|
|
45
|
+
last_progress_loop: 0,
|
|
46
|
+
total_opens: 0,
|
|
47
|
+
reason: ""
|
|
48
|
+
}' > "$CB_STATE_FILE"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
if [[ -f "$CB_HISTORY_FILE" ]]; then
|
|
52
|
+
if ! jq '.' "$CB_HISTORY_FILE" > /dev/null 2>&1; then
|
|
53
|
+
rm -f "$CB_HISTORY_FILE"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ ! -f "$CB_HISTORY_FILE" ]]; then
|
|
58
|
+
echo '[]' > "$CB_HISTORY_FILE"
|
|
59
|
+
fi
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get_circuit_state() {
|
|
63
|
+
if [[ ! -f "$CB_STATE_FILE" ]]; then
|
|
64
|
+
echo "$CB_STATE_CLOSED"
|
|
65
|
+
return
|
|
66
|
+
fi
|
|
67
|
+
jq -r '.state' "$CB_STATE_FILE" 2>/dev/null || echo "$CB_STATE_CLOSED"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
can_execute() {
|
|
71
|
+
local state=$(get_circuit_state)
|
|
72
|
+
if [[ "$state" == "$CB_STATE_OPEN" ]]; then
|
|
73
|
+
return 1
|
|
74
|
+
else
|
|
75
|
+
return 0
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
record_loop_result() {
|
|
80
|
+
local loop_number=$1
|
|
81
|
+
local files_changed=$2
|
|
82
|
+
local has_errors=$3
|
|
83
|
+
local output_length=$4
|
|
84
|
+
|
|
85
|
+
init_circuit_breaker
|
|
86
|
+
|
|
87
|
+
local state_data=$(cat "$CB_STATE_FILE")
|
|
88
|
+
local current_state=$(echo "$state_data" | jq -r '.state')
|
|
89
|
+
local consecutive_no_progress=$(echo "$state_data" | jq -r '.consecutive_no_progress' | tr -d '[:space:]')
|
|
90
|
+
local consecutive_same_error=$(echo "$state_data" | jq -r '.consecutive_same_error' | tr -d '[:space:]')
|
|
91
|
+
local last_progress_loop=$(echo "$state_data" | jq -r '.last_progress_loop' | tr -d '[:space:]')
|
|
92
|
+
|
|
93
|
+
consecutive_no_progress=$((consecutive_no_progress + 0))
|
|
94
|
+
consecutive_same_error=$((consecutive_same_error + 0))
|
|
95
|
+
last_progress_loop=$((last_progress_loop + 0))
|
|
96
|
+
|
|
97
|
+
local has_progress=false
|
|
98
|
+
|
|
99
|
+
if [[ $files_changed -gt 0 ]]; then
|
|
100
|
+
has_progress=true
|
|
101
|
+
consecutive_no_progress=0
|
|
102
|
+
last_progress_loop=$loop_number
|
|
103
|
+
else
|
|
104
|
+
consecutive_no_progress=$((consecutive_no_progress + 1))
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
if [[ "$has_errors" == "true" ]]; then
|
|
108
|
+
consecutive_same_error=$((consecutive_same_error + 1))
|
|
109
|
+
else
|
|
110
|
+
consecutive_same_error=0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
local new_state="$current_state"
|
|
114
|
+
local reason=""
|
|
115
|
+
|
|
116
|
+
case $current_state in
|
|
117
|
+
"$CB_STATE_CLOSED")
|
|
118
|
+
if [[ $consecutive_no_progress -ge $CB_NO_PROGRESS_THRESHOLD ]]; then
|
|
119
|
+
new_state="$CB_STATE_OPEN"
|
|
120
|
+
reason="No progress detected in $consecutive_no_progress consecutive loops"
|
|
121
|
+
elif [[ $consecutive_same_error -ge $CB_SAME_ERROR_THRESHOLD ]]; then
|
|
122
|
+
new_state="$CB_STATE_OPEN"
|
|
123
|
+
reason="Same error repeated in $consecutive_same_error consecutive loops"
|
|
124
|
+
elif [[ $consecutive_no_progress -ge 2 ]]; then
|
|
125
|
+
new_state="$CB_STATE_HALF_OPEN"
|
|
126
|
+
reason="Monitoring: $consecutive_no_progress loops without progress"
|
|
127
|
+
fi
|
|
128
|
+
;;
|
|
129
|
+
"$CB_STATE_HALF_OPEN")
|
|
130
|
+
if [[ "$has_progress" == "true" ]]; then
|
|
131
|
+
new_state="$CB_STATE_CLOSED"
|
|
132
|
+
reason="Progress detected, circuit recovered"
|
|
133
|
+
elif [[ $consecutive_no_progress -ge $CB_NO_PROGRESS_THRESHOLD ]]; then
|
|
134
|
+
new_state="$CB_STATE_OPEN"
|
|
135
|
+
reason="No recovery, opening circuit after $consecutive_no_progress loops"
|
|
136
|
+
fi
|
|
137
|
+
;;
|
|
138
|
+
"$CB_STATE_OPEN")
|
|
139
|
+
reason="Circuit breaker is open, execution halted"
|
|
140
|
+
;;
|
|
141
|
+
esac
|
|
142
|
+
|
|
143
|
+
local total_opens=$(echo "$state_data" | jq -r '.total_opens' | tr -d '[:space:]')
|
|
144
|
+
total_opens=$((total_opens + 0))
|
|
145
|
+
if [[ "$new_state" == "$CB_STATE_OPEN" && "$current_state" != "$CB_STATE_OPEN" ]]; then
|
|
146
|
+
total_opens=$((total_opens + 1))
|
|
147
|
+
fi
|
|
148
|
+
|
|
149
|
+
jq -n \
|
|
150
|
+
--arg state "$new_state" \
|
|
151
|
+
--arg last_change "$(get_iso_timestamp)" \
|
|
152
|
+
--argjson consecutive_no_progress "$consecutive_no_progress" \
|
|
153
|
+
--argjson consecutive_same_error "$consecutive_same_error" \
|
|
154
|
+
--argjson consecutive_permission_denials 0 \
|
|
155
|
+
--argjson last_progress_loop "$last_progress_loop" \
|
|
156
|
+
--argjson total_opens "$total_opens" \
|
|
157
|
+
--arg reason "$reason" \
|
|
158
|
+
--argjson current_loop "$loop_number" \
|
|
159
|
+
'{
|
|
160
|
+
state: $state,
|
|
161
|
+
last_change: $last_change,
|
|
162
|
+
consecutive_no_progress: $consecutive_no_progress,
|
|
163
|
+
consecutive_same_error: $consecutive_same_error,
|
|
164
|
+
consecutive_permission_denials: $consecutive_permission_denials,
|
|
165
|
+
last_progress_loop: $last_progress_loop,
|
|
166
|
+
total_opens: $total_opens,
|
|
167
|
+
reason: $reason,
|
|
168
|
+
current_loop: $current_loop
|
|
169
|
+
}' > "$CB_STATE_FILE"
|
|
170
|
+
|
|
171
|
+
if [[ "$new_state" == "$CB_STATE_OPEN" ]]; then
|
|
172
|
+
return 1
|
|
173
|
+
else
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
reset_circuit_breaker() {
|
|
179
|
+
local reason=${1:-"Manual reset"}
|
|
180
|
+
jq -n \
|
|
181
|
+
--arg state "$CB_STATE_CLOSED" \
|
|
182
|
+
--arg last_change "$(get_iso_timestamp)" \
|
|
183
|
+
--arg reason "$reason" \
|
|
184
|
+
'{
|
|
185
|
+
state: $state,
|
|
186
|
+
last_change: $last_change,
|
|
187
|
+
consecutive_no_progress: 0,
|
|
188
|
+
consecutive_same_error: 0,
|
|
189
|
+
consecutive_permission_denials: 0,
|
|
190
|
+
last_progress_loop: 0,
|
|
191
|
+
total_opens: 0,
|
|
192
|
+
reason: $reason
|
|
193
|
+
}' > "$CB_STATE_FILE"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
should_halt_execution() {
|
|
197
|
+
local state=$(get_circuit_state)
|
|
198
|
+
if [[ "$state" == "$CB_STATE_OPEN" ]]; then
|
|
199
|
+
return 0
|
|
200
|
+
else
|
|
201
|
+
return 1
|
|
202
|
+
fi
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export -f init_circuit_breaker
|
|
206
|
+
export -f get_circuit_state
|
|
207
|
+
export -f can_execute
|
|
208
|
+
export -f record_loop_result
|
|
209
|
+
export -f reset_circuit_breaker
|
|
210
|
+
export -f should_halt_execution
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# date_utils.sh - Cross-platform date utility functions for codeharness Ralph loop
|
|
3
|
+
# Vendored from snarktank/ralph with modifications for codeharness
|
|
4
|
+
|
|
5
|
+
# Get current timestamp in ISO 8601 format with seconds precision
|
|
6
|
+
get_iso_timestamp() {
|
|
7
|
+
local result
|
|
8
|
+
if result=$(date -u -Iseconds 2>/dev/null) && [[ -n "$result" ]]; then
|
|
9
|
+
echo "$result"
|
|
10
|
+
return
|
|
11
|
+
fi
|
|
12
|
+
date -u +"%Y-%m-%dT%H:%M:%S%z" | sed 's/\(..\)$/:\1/'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Get time component (HH:MM:SS) for one hour from now
|
|
16
|
+
get_next_hour_time() {
|
|
17
|
+
if date -d '+1 hour' '+%H:%M:%S' 2>/dev/null; then
|
|
18
|
+
return
|
|
19
|
+
fi
|
|
20
|
+
if date -v+1H '+%H:%M:%S' 2>/dev/null; then
|
|
21
|
+
return
|
|
22
|
+
fi
|
|
23
|
+
local future_epoch=$(($(date +%s) + 3600))
|
|
24
|
+
date -r "$future_epoch" '+%H:%M:%S' 2>/dev/null || date '+%H:%M:%S'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Get current Unix epoch time in seconds
|
|
28
|
+
get_epoch_seconds() {
|
|
29
|
+
date +%s
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# Convert ISO 8601 timestamp to Unix epoch seconds
|
|
33
|
+
parse_iso_to_epoch() {
|
|
34
|
+
local iso_timestamp=$1
|
|
35
|
+
|
|
36
|
+
if [[ -z "$iso_timestamp" || "$iso_timestamp" == "null" ]]; then
|
|
37
|
+
date +%s
|
|
38
|
+
return
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
local result
|
|
42
|
+
if result=$(date -d "$iso_timestamp" +%s 2>/dev/null) && [[ "$result" =~ ^[0-9]+$ ]]; then
|
|
43
|
+
echo "$result"
|
|
44
|
+
return
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
local tz_fixed
|
|
48
|
+
tz_fixed=$(echo "$iso_timestamp" | sed -E 's/Z$/+0000/; s/([+-][0-9]{2}):([0-9]{2})$/\1\2/')
|
|
49
|
+
if result=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$tz_fixed" +%s 2>/dev/null) && [[ "$result" =~ ^[0-9]+$ ]]; then
|
|
50
|
+
echo "$result"
|
|
51
|
+
return
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
date +%s
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export -f get_iso_timestamp
|
|
58
|
+
export -f get_next_hour_time
|
|
59
|
+
export -f get_epoch_seconds
|
|
60
|
+
export -f parse_iso_to_epoch
|