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.
@@ -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