bmalph 2.7.3 → 2.7.5
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/README.md +68 -30
- package/dist/commands/doctor-checks.js +5 -4
- package/dist/commands/doctor-checks.js.map +1 -1
- package/dist/commands/run.js +4 -0
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/status.js +12 -3
- package/dist/commands/status.js.map +1 -1
- package/dist/installer.js +101 -48
- package/dist/installer.js.map +1 -1
- package/dist/platform/cursor-runtime-checks.js +81 -0
- package/dist/platform/cursor-runtime-checks.js.map +1 -0
- package/dist/platform/cursor.js +4 -3
- package/dist/platform/cursor.js.map +1 -1
- package/dist/platform/detect.js +28 -5
- package/dist/platform/detect.js.map +1 -1
- package/dist/platform/instructions-snippet.js +18 -0
- package/dist/platform/instructions-snippet.js.map +1 -1
- package/dist/platform/resolve.js +23 -5
- package/dist/platform/resolve.js.map +1 -1
- package/dist/run/ralph-process.js +84 -15
- package/dist/run/ralph-process.js.map +1 -1
- package/dist/transition/artifact-scan.js +15 -3
- package/dist/transition/artifact-scan.js.map +1 -1
- package/dist/transition/fix-plan.js +4 -4
- package/dist/transition/fix-plan.js.map +1 -1
- package/dist/transition/orchestration.js +45 -13
- package/dist/transition/orchestration.js.map +1 -1
- package/dist/transition/preflight.js +9 -1
- package/dist/transition/preflight.js.map +1 -1
- package/dist/transition/story-id.js +46 -0
- package/dist/transition/story-id.js.map +1 -0
- package/dist/transition/story-parsing.js +3 -4
- package/dist/transition/story-parsing.js.map +1 -1
- package/package.json +1 -1
- package/ralph/RALPH-REFERENCE.md +27 -3
- package/ralph/drivers/claude-code.sh +44 -2
- package/ralph/drivers/codex.sh +10 -1
- package/ralph/drivers/copilot.sh +5 -0
- package/ralph/drivers/cursor.sh +50 -29
- package/ralph/lib/response_analyzer.sh +440 -111
- package/ralph/ralph_loop.sh +73 -61
|
@@ -27,8 +27,215 @@ NO_WORK_PATTERNS=("nothing to do" "no changes" "already implemented" "up to date
|
|
|
27
27
|
# JSON OUTPUT FORMAT DETECTION AND PARSING
|
|
28
28
|
# =============================================================================
|
|
29
29
|
|
|
30
|
+
# Windows jq.exe handles workspace-relative paths more reliably than POSIX
|
|
31
|
+
# absolute temp paths like /tmp/... when invoked from Git Bash.
|
|
32
|
+
create_jq_temp_file() {
|
|
33
|
+
mktemp "./.response_analyzer.XXXXXX"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Count parseable top-level JSON documents in an output file.
|
|
37
|
+
count_json_documents() {
|
|
38
|
+
local output_file=$1
|
|
39
|
+
|
|
40
|
+
if [[ ! -f "$output_file" ]] || [[ ! -s "$output_file" ]]; then
|
|
41
|
+
echo "0"
|
|
42
|
+
return 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
jq -n -j 'reduce inputs as $item (0; . + 1)' < "$output_file" 2>/dev/null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Normalize a Claude CLI array response into a single object file.
|
|
49
|
+
normalize_cli_array_response() {
|
|
50
|
+
local output_file=$1
|
|
51
|
+
local normalized_file=$2
|
|
52
|
+
|
|
53
|
+
# Extract the "result" type message from the array (usually the last entry)
|
|
54
|
+
# This contains: result, session_id, is_error, duration_ms, etc.
|
|
55
|
+
local result_obj=$(jq '[.[] | select(.type == "result")] | .[-1] // {}' "$output_file" 2>/dev/null)
|
|
56
|
+
|
|
57
|
+
# Guard against empty result_obj if jq fails (review fix: Macroscope)
|
|
58
|
+
[[ -z "$result_obj" ]] && result_obj="{}"
|
|
59
|
+
|
|
60
|
+
# Extract session_id from init message as fallback
|
|
61
|
+
local init_session_id=$(jq -r '.[] | select(.type == "system" and .subtype == "init") | .session_id // empty' "$output_file" 2>/dev/null | head -1 | tr -d '\r')
|
|
62
|
+
|
|
63
|
+
# Prioritize result object's own session_id, then fall back to init message (review fix: CodeRabbit)
|
|
64
|
+
# This prevents session ID loss when arrays lack an init message with session_id
|
|
65
|
+
local effective_session_id
|
|
66
|
+
effective_session_id=$(echo "$result_obj" | jq -r -j '.sessionId // .session_id // empty' 2>/dev/null)
|
|
67
|
+
if [[ -z "$effective_session_id" || "$effective_session_id" == "null" ]]; then
|
|
68
|
+
effective_session_id="$init_session_id"
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Build normalized object merging result with effective session_id
|
|
72
|
+
if [[ -n "$effective_session_id" && "$effective_session_id" != "null" ]]; then
|
|
73
|
+
echo "$result_obj" | jq --arg sid "$effective_session_id" '. + {sessionId: $sid} | del(.session_id)' > "$normalized_file"
|
|
74
|
+
else
|
|
75
|
+
echo "$result_obj" | jq 'del(.session_id)' > "$normalized_file"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Normalize Codex JSONL event output into the object shape expected downstream.
|
|
80
|
+
normalize_codex_jsonl_response() {
|
|
81
|
+
local output_file=$1
|
|
82
|
+
local normalized_file=$2
|
|
83
|
+
|
|
84
|
+
jq -rs '
|
|
85
|
+
def agent_text($item):
|
|
86
|
+
$item.text // (
|
|
87
|
+
[($item.content // [])[]? | select(.type == "output_text") | .text]
|
|
88
|
+
| join("\n")
|
|
89
|
+
) // "";
|
|
90
|
+
|
|
91
|
+
(map(select(.type == "item.completed" and .item.type == "agent_message")) | last | .item // {}) as $agent_message
|
|
92
|
+
| {
|
|
93
|
+
result: agent_text($agent_message),
|
|
94
|
+
sessionId: (map(select(.type == "thread.started") | .thread_id // empty) | first // ""),
|
|
95
|
+
metadata: {}
|
|
96
|
+
}
|
|
97
|
+
' "$output_file" > "$normalized_file"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# Normalize Cursor stream-json event output into the object shape expected downstream.
|
|
101
|
+
normalize_cursor_stream_json_response() {
|
|
102
|
+
local output_file=$1
|
|
103
|
+
local normalized_file=$2
|
|
104
|
+
|
|
105
|
+
jq -rs '
|
|
106
|
+
def assistant_text($item):
|
|
107
|
+
[($item.message.content // [])[]? | select(.type == "text") | .text]
|
|
108
|
+
| join("\n");
|
|
109
|
+
|
|
110
|
+
(map(select(.type == "result")) | last // {}) as $result_event
|
|
111
|
+
| {
|
|
112
|
+
result: (
|
|
113
|
+
$result_event.result
|
|
114
|
+
// (
|
|
115
|
+
map(select(.type == "assistant"))
|
|
116
|
+
| map(assistant_text(.))
|
|
117
|
+
| map(select(length > 0))
|
|
118
|
+
| join("\n")
|
|
119
|
+
)
|
|
120
|
+
),
|
|
121
|
+
sessionId: (
|
|
122
|
+
$result_event.session_id
|
|
123
|
+
// (map(select(.type == "system" and .subtype == "init") | .session_id // empty) | first)
|
|
124
|
+
// ""
|
|
125
|
+
),
|
|
126
|
+
metadata: {}
|
|
127
|
+
}
|
|
128
|
+
' "$output_file" > "$normalized_file"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Detect whether a multi-document stream matches Codex JSONL events.
|
|
132
|
+
is_codex_jsonl_output() {
|
|
133
|
+
local output_file=$1
|
|
134
|
+
|
|
135
|
+
jq -n -j '
|
|
136
|
+
reduce inputs as $item (
|
|
137
|
+
false;
|
|
138
|
+
. or (
|
|
139
|
+
$item.type == "thread.started" or
|
|
140
|
+
($item.type == "item.completed" and ($item.item.type? != null))
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
' < "$output_file" 2>/dev/null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
# Detect whether a multi-document stream matches Cursor stream-json events.
|
|
147
|
+
is_cursor_stream_json_output() {
|
|
148
|
+
local output_file=$1
|
|
149
|
+
|
|
150
|
+
jq -n -j '
|
|
151
|
+
reduce inputs as $item (
|
|
152
|
+
false;
|
|
153
|
+
. or (
|
|
154
|
+
$item.type == "system" or
|
|
155
|
+
$item.type == "user" or
|
|
156
|
+
$item.type == "assistant" or
|
|
157
|
+
$item.type == "tool_call" or
|
|
158
|
+
$item.type == "result"
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
' < "$output_file" 2>/dev/null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
# Normalize structured output to a single object when downstream parsing expects one.
|
|
165
|
+
normalize_json_output() {
|
|
166
|
+
local output_file=$1
|
|
167
|
+
local normalized_file=$2
|
|
168
|
+
local json_document_count
|
|
169
|
+
|
|
170
|
+
json_document_count=$(count_json_documents "$output_file") || return 1
|
|
171
|
+
|
|
172
|
+
if [[ "$json_document_count" -gt 1 ]]; then
|
|
173
|
+
local is_codex_jsonl
|
|
174
|
+
is_codex_jsonl=$(is_codex_jsonl_output "$output_file") || return 1
|
|
175
|
+
|
|
176
|
+
if [[ "$is_codex_jsonl" == "true" ]]; then
|
|
177
|
+
normalize_codex_jsonl_response "$output_file" "$normalized_file"
|
|
178
|
+
return $?
|
|
179
|
+
fi
|
|
180
|
+
|
|
181
|
+
local is_cursor_stream_json
|
|
182
|
+
is_cursor_stream_json=$(is_cursor_stream_json_output "$output_file") || return 1
|
|
183
|
+
|
|
184
|
+
if [[ "$is_cursor_stream_json" == "true" ]]; then
|
|
185
|
+
normalize_cursor_stream_json_response "$output_file" "$normalized_file"
|
|
186
|
+
return $?
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
if jq -e 'type == "array"' "$output_file" >/dev/null 2>&1; then
|
|
193
|
+
normalize_cli_array_response "$output_file" "$normalized_file"
|
|
194
|
+
return $?
|
|
195
|
+
fi
|
|
196
|
+
|
|
197
|
+
return 1
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Extract persisted session ID from any supported structured output.
|
|
201
|
+
extract_session_id_from_output() {
|
|
202
|
+
local output_file=$1
|
|
203
|
+
local normalized_file=""
|
|
204
|
+
local session_id=""
|
|
205
|
+
local json_document_count
|
|
206
|
+
|
|
207
|
+
if [[ ! -f "$output_file" ]] || [[ ! -s "$output_file" ]]; then
|
|
208
|
+
echo ""
|
|
209
|
+
return 1
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
json_document_count=$(count_json_documents "$output_file") || {
|
|
213
|
+
echo ""
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if [[ "$json_document_count" -gt 1 ]] || jq -e 'type == "array"' "$output_file" >/dev/null 2>&1; then
|
|
218
|
+
normalized_file=$(create_jq_temp_file)
|
|
219
|
+
if ! normalize_json_output "$output_file" "$normalized_file"; then
|
|
220
|
+
rm -f "$normalized_file"
|
|
221
|
+
echo ""
|
|
222
|
+
return 1
|
|
223
|
+
fi
|
|
224
|
+
output_file="$normalized_file"
|
|
225
|
+
fi
|
|
226
|
+
|
|
227
|
+
session_id=$(jq -r '.sessionId // .metadata.session_id // .session_id // empty' "$output_file" 2>/dev/null | head -1 | tr -d '\r')
|
|
228
|
+
|
|
229
|
+
if [[ -n "$normalized_file" && -f "$normalized_file" ]]; then
|
|
230
|
+
rm -f "$normalized_file"
|
|
231
|
+
fi
|
|
232
|
+
|
|
233
|
+
echo "$session_id"
|
|
234
|
+
[[ -n "$session_id" && "$session_id" != "null" ]]
|
|
235
|
+
}
|
|
236
|
+
|
|
30
237
|
# Detect output format (json or text)
|
|
31
|
-
# Returns: "json"
|
|
238
|
+
# Returns: "json" for single-document JSON and newline-delimited JSON, "text" otherwise
|
|
32
239
|
detect_output_format() {
|
|
33
240
|
local output_file=$1
|
|
34
241
|
|
|
@@ -45,24 +252,68 @@ detect_output_format() {
|
|
|
45
252
|
return
|
|
46
253
|
fi
|
|
47
254
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
echo "json"
|
|
51
|
-
else
|
|
255
|
+
local json_document_count
|
|
256
|
+
json_document_count=$(count_json_documents "$output_file") || {
|
|
52
257
|
echo "text"
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if [[ "$json_document_count" -eq 1 ]]; then
|
|
262
|
+
echo "json"
|
|
263
|
+
return
|
|
53
264
|
fi
|
|
265
|
+
|
|
266
|
+
if [[ "$json_document_count" -gt 1 ]]; then
|
|
267
|
+
local is_codex_jsonl
|
|
268
|
+
is_codex_jsonl=$(is_codex_jsonl_output "$output_file") || {
|
|
269
|
+
echo "text"
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if [[ "$is_codex_jsonl" == "true" ]]; then
|
|
274
|
+
echo "json"
|
|
275
|
+
return
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
local is_cursor_stream_json
|
|
279
|
+
is_cursor_stream_json=$(is_cursor_stream_json_output "$output_file") || {
|
|
280
|
+
echo "text"
|
|
281
|
+
return
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if [[ "$is_cursor_stream_json" == "true" ]]; then
|
|
285
|
+
echo "json"
|
|
286
|
+
return
|
|
287
|
+
fi
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
echo "text"
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
trim_shell_whitespace() {
|
|
294
|
+
local value="${1//$'\r'/}"
|
|
295
|
+
|
|
296
|
+
value="${value#"${value%%[![:space:]]*}"}"
|
|
297
|
+
value="${value%"${value##*[![:space:]]}"}"
|
|
298
|
+
|
|
299
|
+
printf '%s' "$value"
|
|
54
300
|
}
|
|
55
301
|
|
|
56
302
|
# Parse JSON response and extract structured fields
|
|
57
303
|
# Creates .ralph/.json_parse_result with normalized analysis data
|
|
58
|
-
# Supports
|
|
304
|
+
# Supports FIVE JSON formats:
|
|
59
305
|
# 1. Flat format: { status, exit_signal, work_type, files_modified, ... }
|
|
60
306
|
# 2. Claude CLI object format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } }
|
|
61
307
|
# 3. Claude CLI array format: [ {type: "system", ...}, {type: "assistant", ...}, {type: "result", ...} ]
|
|
308
|
+
# 4. Codex JSONL format: {"type":"thread.started",...}\n{"type":"item.completed","item":{...}}
|
|
309
|
+
# 5. Cursor stream-json format: {"type":"assistant",...}\n{"type":"result",...}
|
|
62
310
|
parse_json_response() {
|
|
63
311
|
local output_file=$1
|
|
64
312
|
local result_file="${2:-$RALPH_DIR/.json_parse_result}"
|
|
313
|
+
local original_output_file=$output_file
|
|
65
314
|
local normalized_file=""
|
|
315
|
+
local json_document_count=""
|
|
316
|
+
local response_shape="object"
|
|
66
317
|
|
|
67
318
|
if [[ ! -f "$output_file" ]]; then
|
|
68
319
|
echo "ERROR: Output file not found: $output_file" >&2
|
|
@@ -70,71 +321,63 @@ parse_json_response() {
|
|
|
70
321
|
fi
|
|
71
322
|
|
|
72
323
|
# Validate JSON first
|
|
73
|
-
|
|
324
|
+
json_document_count=$(count_json_documents "$output_file") || {
|
|
74
325
|
echo "ERROR: Invalid JSON in output file" >&2
|
|
75
326
|
return 1
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
# Check if JSON is an array (Claude CLI array format)
|
|
79
|
-
# Claude CLI outputs: [{type: "system", ...}, {type: "assistant", ...}, {type: "result", ...}]
|
|
80
|
-
if jq -e 'type == "array"' "$output_file" >/dev/null 2>&1; then
|
|
81
|
-
normalized_file=$(mktemp)
|
|
82
|
-
|
|
83
|
-
# Extract the "result" type message from the array (usually the last entry)
|
|
84
|
-
# This contains: result, session_id, is_error, duration_ms, etc.
|
|
85
|
-
local result_obj=$(jq '[.[] | select(.type == "result")] | .[-1] // {}' "$output_file" 2>/dev/null)
|
|
327
|
+
}
|
|
86
328
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
local init_session_id=$(jq -r '.[] | select(.type == "system" and .subtype == "init") | .session_id // empty' "$output_file" 2>/dev/null | head -1)
|
|
92
|
-
|
|
93
|
-
# Prioritize result object's own session_id, then fall back to init message (review fix: CodeRabbit)
|
|
94
|
-
# This prevents session ID loss when arrays lack an init message with session_id
|
|
95
|
-
local effective_session_id
|
|
96
|
-
effective_session_id=$(echo "$result_obj" | jq -r '.sessionId // .session_id // empty' 2>/dev/null)
|
|
97
|
-
if [[ -z "$effective_session_id" || "$effective_session_id" == "null" ]]; then
|
|
98
|
-
effective_session_id="$init_session_id"
|
|
99
|
-
fi
|
|
100
|
-
|
|
101
|
-
# Build normalized object merging result with effective session_id
|
|
102
|
-
if [[ -n "$effective_session_id" && "$effective_session_id" != "null" ]]; then
|
|
103
|
-
echo "$result_obj" | jq --arg sid "$effective_session_id" '. + {sessionId: $sid} | del(.session_id)' > "$normalized_file"
|
|
329
|
+
# Normalize multi-document JSONL and array responses to a single object.
|
|
330
|
+
if [[ "$json_document_count" -gt 1 ]] || jq -e 'type == "array"' "$output_file" >/dev/null 2>&1; then
|
|
331
|
+
if [[ "$json_document_count" -gt 1 ]]; then
|
|
332
|
+
response_shape="jsonl"
|
|
104
333
|
else
|
|
105
|
-
|
|
334
|
+
response_shape="array"
|
|
335
|
+
fi
|
|
336
|
+
normalized_file=$(create_jq_temp_file)
|
|
337
|
+
if ! normalize_json_output "$output_file" "$normalized_file"; then
|
|
338
|
+
rm -f "$normalized_file"
|
|
339
|
+
echo "ERROR: Failed to normalize JSON output" >&2
|
|
340
|
+
return 1
|
|
106
341
|
fi
|
|
107
342
|
|
|
108
343
|
# Use normalized file for subsequent parsing
|
|
109
344
|
output_file="$normalized_file"
|
|
345
|
+
|
|
346
|
+
if [[ "$response_shape" == "jsonl" ]]; then
|
|
347
|
+
if is_codex_jsonl_output "$original_output_file" >/dev/null 2>&1; then
|
|
348
|
+
response_shape="codex_jsonl"
|
|
349
|
+
else
|
|
350
|
+
response_shape="cursor_stream_jsonl"
|
|
351
|
+
fi
|
|
352
|
+
fi
|
|
110
353
|
fi
|
|
111
354
|
|
|
112
355
|
# Detect JSON format by checking for Claude CLI fields
|
|
113
|
-
local has_result_field=$(jq -r 'has("result")' "$output_file" 2>/dev/null)
|
|
356
|
+
local has_result_field=$(jq -r -j 'has("result")' "$output_file" 2>/dev/null)
|
|
114
357
|
|
|
115
358
|
# Extract fields - support both flat format and Claude CLI format
|
|
116
359
|
# Priority: Claude CLI fields first, then flat format fields
|
|
117
360
|
|
|
118
361
|
# Status: from flat format OR derived from metadata.completion_status
|
|
119
|
-
local status=$(jq -r '.status // "UNKNOWN"' "$output_file" 2>/dev/null)
|
|
120
|
-
local completion_status=$(jq -r '.metadata.completion_status // ""' "$output_file" 2>/dev/null)
|
|
362
|
+
local status=$(jq -r -j '.status // "UNKNOWN"' "$output_file" 2>/dev/null)
|
|
363
|
+
local completion_status=$(jq -r -j '.metadata.completion_status // ""' "$output_file" 2>/dev/null)
|
|
121
364
|
if [[ "$completion_status" == "complete" || "$completion_status" == "COMPLETE" ]]; then
|
|
122
365
|
status="COMPLETE"
|
|
123
366
|
fi
|
|
124
367
|
|
|
125
368
|
# Exit signal: from flat format OR derived from completion_status
|
|
126
369
|
# Track whether EXIT_SIGNAL was explicitly provided (vs inferred from STATUS)
|
|
127
|
-
local exit_signal=$(jq -r '.exit_signal // false' "$output_file" 2>/dev/null)
|
|
128
|
-
local explicit_exit_signal_found=$(jq -r 'has("exit_signal")' "$output_file" 2>/dev/null)
|
|
370
|
+
local exit_signal=$(jq -r -j '.exit_signal // false' "$output_file" 2>/dev/null)
|
|
371
|
+
local explicit_exit_signal_found=$(jq -r -j 'has("exit_signal")' "$output_file" 2>/dev/null)
|
|
129
372
|
|
|
130
373
|
# Bug #1 Fix: If exit_signal is still false, check for RALPH_STATUS block in .result field
|
|
131
374
|
# Claude CLI JSON format embeds the RALPH_STATUS block within the .result text field
|
|
132
375
|
if [[ "$exit_signal" == "false" && "$has_result_field" == "true" ]]; then
|
|
133
|
-
local result_text=$(jq -r '.result // ""' "$output_file" 2>/dev/null)
|
|
376
|
+
local result_text=$(jq -r -j '.result // ""' "$output_file" 2>/dev/null)
|
|
134
377
|
if [[ -n "$result_text" ]] && echo "$result_text" | grep -q -- "---RALPH_STATUS---"; then
|
|
135
378
|
# Extract EXIT_SIGNAL value from RALPH_STATUS block within result text
|
|
136
379
|
local embedded_exit_sig
|
|
137
|
-
embedded_exit_sig=$(
|
|
380
|
+
embedded_exit_sig=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2)")
|
|
138
381
|
if [[ -n "$embedded_exit_sig" ]]; then
|
|
139
382
|
# Explicit EXIT_SIGNAL found in RALPH_STATUS block
|
|
140
383
|
explicit_exit_signal_found="true"
|
|
@@ -149,7 +392,7 @@ parse_json_response() {
|
|
|
149
392
|
# Also check STATUS field as fallback ONLY when EXIT_SIGNAL was not specified
|
|
150
393
|
# This respects explicit EXIT_SIGNAL: false which means "task complete, continue working"
|
|
151
394
|
local embedded_status
|
|
152
|
-
embedded_status=$(
|
|
395
|
+
embedded_status=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "STATUS:" | cut -d: -f2)")
|
|
153
396
|
if [[ "$embedded_status" == "COMPLETE" && "$explicit_exit_signal_found" != "true" ]]; then
|
|
154
397
|
# STATUS: COMPLETE without any EXIT_SIGNAL field implies completion
|
|
155
398
|
exit_signal="true"
|
|
@@ -159,40 +402,40 @@ parse_json_response() {
|
|
|
159
402
|
fi
|
|
160
403
|
|
|
161
404
|
# Work type: from flat format
|
|
162
|
-
local work_type=$(jq -r '.work_type // "UNKNOWN"' "$output_file" 2>/dev/null)
|
|
405
|
+
local work_type=$(jq -r -j '.work_type // "UNKNOWN"' "$output_file" 2>/dev/null)
|
|
163
406
|
|
|
164
407
|
# Files modified: from flat format OR from metadata.files_changed
|
|
165
|
-
local files_modified=$(jq -r '.metadata.files_changed // .files_modified // 0' "$output_file" 2>/dev/null)
|
|
408
|
+
local files_modified=$(jq -r -j '.metadata.files_changed // .files_modified // 0' "$output_file" 2>/dev/null)
|
|
166
409
|
|
|
167
410
|
# Error count: from flat format OR derived from metadata.has_errors
|
|
168
411
|
# Note: When only has_errors=true is present (without explicit error_count),
|
|
169
412
|
# we set error_count=1 as a minimum. This is defensive programming since
|
|
170
413
|
# the stuck detection threshold is >5 errors, so 1 error won't trigger it.
|
|
171
414
|
# Actual error count may be higher, but precise count isn't critical for our logic.
|
|
172
|
-
local error_count=$(jq -r '.error_count // 0' "$output_file" 2>/dev/null)
|
|
173
|
-
local has_errors=$(jq -r '.metadata.has_errors // false' "$output_file" 2>/dev/null)
|
|
415
|
+
local error_count=$(jq -r -j '.error_count // 0' "$output_file" 2>/dev/null)
|
|
416
|
+
local has_errors=$(jq -r -j '.metadata.has_errors // false' "$output_file" 2>/dev/null)
|
|
174
417
|
if [[ "$has_errors" == "true" && "$error_count" == "0" ]]; then
|
|
175
418
|
error_count=1 # At least one error if has_errors is true
|
|
176
419
|
fi
|
|
177
420
|
|
|
178
421
|
# Summary: from flat format OR from result field (Claude CLI format)
|
|
179
|
-
local summary=$(jq -r '.result // .summary // ""' "$output_file" 2>/dev/null)
|
|
422
|
+
local summary=$(jq -r -j '.result // .summary // ""' "$output_file" 2>/dev/null)
|
|
180
423
|
|
|
181
424
|
# Session ID: from Claude CLI format (sessionId) OR from metadata.session_id
|
|
182
|
-
local session_id=$(jq -r '.sessionId // .metadata.session_id // ""' "$output_file" 2>/dev/null)
|
|
425
|
+
local session_id=$(jq -r -j '.sessionId // .metadata.session_id // .session_id // ""' "$output_file" 2>/dev/null)
|
|
183
426
|
|
|
184
427
|
# Loop number: from metadata
|
|
185
|
-
local loop_number=$(jq -r '.metadata.loop_number // .loop_number // 0' "$output_file" 2>/dev/null)
|
|
428
|
+
local loop_number=$(jq -r -j '.metadata.loop_number // .loop_number // 0' "$output_file" 2>/dev/null)
|
|
186
429
|
|
|
187
430
|
# Confidence: from flat format
|
|
188
|
-
local confidence=$(jq -r '.confidence // 0' "$output_file" 2>/dev/null)
|
|
431
|
+
local confidence=$(jq -r -j '.confidence // 0' "$output_file" 2>/dev/null)
|
|
189
432
|
|
|
190
433
|
# Progress indicators: from Claude CLI metadata (optional)
|
|
191
|
-
local progress_count=$(jq -r '.metadata.progress_indicators | if . then length else 0 end' "$output_file" 2>/dev/null)
|
|
434
|
+
local progress_count=$(jq -r -j '.metadata.progress_indicators | if . then length else 0 end' "$output_file" 2>/dev/null)
|
|
192
435
|
|
|
193
436
|
# Permission denials: from Claude Code output (Issue #101)
|
|
194
437
|
# When Claude Code is denied permission to run commands, it outputs a permission_denials array
|
|
195
|
-
local permission_denial_count=$(jq -r '.permission_denials | if . then length else 0 end' "$output_file" 2>/dev/null)
|
|
438
|
+
local permission_denial_count=$(jq -r -j '.permission_denials | if . then length else 0 end' "$output_file" 2>/dev/null)
|
|
196
439
|
permission_denial_count=$((permission_denial_count + 0)) # Ensure integer
|
|
197
440
|
|
|
198
441
|
local has_permission_denials="false"
|
|
@@ -206,7 +449,27 @@ parse_json_response() {
|
|
|
206
449
|
# while Bash denial shows "Bash(git commit -m ...)" with truncated command
|
|
207
450
|
local denied_commands_json="[]"
|
|
208
451
|
if [[ $permission_denial_count -gt 0 ]]; then
|
|
209
|
-
denied_commands_json=$(jq -r '[.permission_denials[] | if .tool_name == "Bash" then "Bash(\(.tool_input.command // "?" | split("\n")[0] | .[0:60]))" else .tool_name // "unknown" end]' "$output_file" 2>/dev/null || echo "[]")
|
|
452
|
+
denied_commands_json=$(jq -r -j '[.permission_denials[] | if .tool_name == "Bash" then "Bash(\(.tool_input.command // "?" | split("\n")[0] | .[0:60]))" else .tool_name // "unknown" end]' "$output_file" 2>/dev/null || echo "[]")
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
# Apply completion heuristics to normalized summary text when explicit structured
|
|
456
|
+
# completion markers are absent. This keeps JSONL analysis aligned with text mode.
|
|
457
|
+
local summary_has_completion_keyword="false"
|
|
458
|
+
local summary_has_no_work_pattern="false"
|
|
459
|
+
if [[ "$response_shape" == "codex_jsonl" || "$response_shape" == "cursor_stream_jsonl" ]] && [[ "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
|
|
460
|
+
for keyword in "${COMPLETION_KEYWORDS[@]}"; do
|
|
461
|
+
if echo "$summary" | grep -qi "$keyword"; then
|
|
462
|
+
summary_has_completion_keyword="true"
|
|
463
|
+
break
|
|
464
|
+
fi
|
|
465
|
+
done
|
|
466
|
+
|
|
467
|
+
for pattern in "${NO_WORK_PATTERNS[@]}"; do
|
|
468
|
+
if echo "$summary" | grep -qi "$pattern"; then
|
|
469
|
+
summary_has_no_work_pattern="true"
|
|
470
|
+
break
|
|
471
|
+
fi
|
|
472
|
+
done
|
|
210
473
|
fi
|
|
211
474
|
|
|
212
475
|
# Normalize values
|
|
@@ -215,7 +478,7 @@ parse_json_response() {
|
|
|
215
478
|
if [[ "$explicit_exit_signal_found" == "true" ]]; then
|
|
216
479
|
# Respect explicit EXIT_SIGNAL value (already set above)
|
|
217
480
|
[[ "$exit_signal" == "true" ]] && exit_signal="true" || exit_signal="false"
|
|
218
|
-
elif [[ "$exit_signal" == "true" || "$status" == "COMPLETE" || "$completion_status" == "complete" || "$completion_status" == "COMPLETE" ]]; then
|
|
481
|
+
elif [[ "$exit_signal" == "true" || "$status" == "COMPLETE" || "$completion_status" == "complete" || "$completion_status" == "COMPLETE" || "$summary_has_completion_keyword" == "true" || "$summary_has_no_work_pattern" == "true" ]]; then
|
|
219
482
|
exit_signal="true"
|
|
220
483
|
else
|
|
221
484
|
exit_signal="false"
|
|
@@ -242,7 +505,11 @@ parse_json_response() {
|
|
|
242
505
|
|
|
243
506
|
# Calculate has_completion_signal
|
|
244
507
|
local has_completion_signal="false"
|
|
245
|
-
if [[ "$
|
|
508
|
+
if [[ "$explicit_exit_signal_found" == "true" ]]; then
|
|
509
|
+
if [[ "$exit_signal" == "true" ]]; then
|
|
510
|
+
has_completion_signal="true"
|
|
511
|
+
fi
|
|
512
|
+
elif [[ "$status" == "COMPLETE" || "$exit_signal" == "true" || "$summary_has_completion_keyword" == "true" || "$summary_has_no_work_pattern" == "true" ]]; then
|
|
246
513
|
has_completion_signal="true"
|
|
247
514
|
fi
|
|
248
515
|
|
|
@@ -327,24 +594,26 @@ analyze_response() {
|
|
|
327
594
|
|
|
328
595
|
# Detect output format and try JSON parsing first
|
|
329
596
|
local output_format=$(detect_output_format "$output_file")
|
|
597
|
+
local json_parse_result_file=""
|
|
330
598
|
|
|
331
599
|
if [[ "$output_format" == "json" ]]; then
|
|
332
600
|
# Try JSON parsing
|
|
333
|
-
|
|
601
|
+
json_parse_result_file=$(create_jq_temp_file)
|
|
602
|
+
if parse_json_response "$output_file" "$json_parse_result_file" 2>/dev/null; then
|
|
334
603
|
# Extract values from JSON parse result
|
|
335
|
-
has_completion_signal=$(jq -r '.has_completion_signal' $
|
|
336
|
-
exit_signal=$(jq -r '.exit_signal' $
|
|
337
|
-
is_test_only=$(jq -r '.is_test_only' $
|
|
338
|
-
is_stuck=$(jq -r '.is_stuck' $
|
|
339
|
-
work_summary=$(jq -r '.summary' $
|
|
340
|
-
files_modified=$(jq -r '.files_modified' $
|
|
341
|
-
local json_confidence=$(jq -r '.confidence' $
|
|
342
|
-
local session_id=$(jq -r '.session_id' $
|
|
604
|
+
has_completion_signal=$(jq -r -j '.has_completion_signal' "$json_parse_result_file" 2>/dev/null || echo "false")
|
|
605
|
+
exit_signal=$(jq -r -j '.exit_signal' "$json_parse_result_file" 2>/dev/null || echo "false")
|
|
606
|
+
is_test_only=$(jq -r -j '.is_test_only' "$json_parse_result_file" 2>/dev/null || echo "false")
|
|
607
|
+
is_stuck=$(jq -r -j '.is_stuck' "$json_parse_result_file" 2>/dev/null || echo "false")
|
|
608
|
+
work_summary=$(jq -r -j '.summary' "$json_parse_result_file" 2>/dev/null || echo "")
|
|
609
|
+
files_modified=$(jq -r -j '.files_modified' "$json_parse_result_file" 2>/dev/null || echo "0")
|
|
610
|
+
local json_confidence=$(jq -r -j '.confidence' "$json_parse_result_file" 2>/dev/null || echo "0")
|
|
611
|
+
local session_id=$(jq -r -j '.session_id' "$json_parse_result_file" 2>/dev/null || echo "")
|
|
343
612
|
|
|
344
613
|
# Extract permission denial fields (Issue #101)
|
|
345
|
-
local has_permission_denials=$(jq -r '.has_permission_denials' $
|
|
346
|
-
local permission_denial_count=$(jq -r '.permission_denial_count' $
|
|
347
|
-
local denied_commands_json=$(jq -r '.denied_commands' $
|
|
614
|
+
local has_permission_denials=$(jq -r -j '.has_permission_denials' "$json_parse_result_file" 2>/dev/null || echo "false")
|
|
615
|
+
local permission_denial_count=$(jq -r -j '.permission_denial_count' "$json_parse_result_file" 2>/dev/null || echo "0")
|
|
616
|
+
local denied_commands_json=$(jq -r -j '.denied_commands' "$json_parse_result_file" 2>/dev/null || echo "[]")
|
|
348
617
|
|
|
349
618
|
# Persist session ID if present (for session continuity across loop iterations)
|
|
350
619
|
if [[ -n "$session_id" && "$session_id" != "null" ]]; then
|
|
@@ -435,9 +704,10 @@ analyze_response() {
|
|
|
435
704
|
denied_commands: $denied_commands
|
|
436
705
|
}
|
|
437
706
|
}' > "$analysis_result_file"
|
|
438
|
-
rm -f "$
|
|
707
|
+
rm -f "$json_parse_result_file"
|
|
439
708
|
return 0
|
|
440
709
|
fi
|
|
710
|
+
rm -f "$json_parse_result_file"
|
|
441
711
|
# If JSON parsing failed, fall through to text parsing
|
|
442
712
|
fi
|
|
443
713
|
|
|
@@ -450,8 +720,8 @@ analyze_response() {
|
|
|
450
720
|
# 1. Check for explicit structured output (if Claude follows schema)
|
|
451
721
|
if grep -q -- "---RALPH_STATUS---" "$output_file"; then
|
|
452
722
|
# Parse structured output
|
|
453
|
-
local status=$(grep "STATUS:" "$output_file" | cut -d: -f2
|
|
454
|
-
local exit_sig=$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2
|
|
723
|
+
local status=$(trim_shell_whitespace "$(grep "STATUS:" "$output_file" | cut -d: -f2)")
|
|
724
|
+
local exit_sig=$(trim_shell_whitespace "$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2)")
|
|
455
725
|
|
|
456
726
|
# If EXIT_SIGNAL is explicitly provided, respect it
|
|
457
727
|
if [[ -n "$exit_sig" ]]; then
|
|
@@ -588,6 +858,12 @@ analyze_response() {
|
|
|
588
858
|
fi
|
|
589
859
|
fi
|
|
590
860
|
|
|
861
|
+
# Explicit EXIT_SIGNAL=false means "continue working", so completion
|
|
862
|
+
# heuristics must not register a done signal.
|
|
863
|
+
if [[ "$explicit_exit_signal_found" == "true" && "$exit_signal" == "false" ]]; then
|
|
864
|
+
has_completion_signal=false
|
|
865
|
+
fi
|
|
866
|
+
|
|
591
867
|
# 9. Determine exit signal based on confidence (heuristic)
|
|
592
868
|
# IMPORTANT: Only apply heuristics if no explicit EXIT_SIGNAL was found in RALPH_STATUS
|
|
593
869
|
# Claude's explicit intent takes precedence over natural language pattern matching
|
|
@@ -650,10 +926,10 @@ update_exit_signals() {
|
|
|
650
926
|
fi
|
|
651
927
|
|
|
652
928
|
# Read analysis results
|
|
653
|
-
local is_test_only=$(jq -r '.analysis.is_test_only' "$analysis_file")
|
|
654
|
-
local has_completion_signal=$(jq -r '.analysis.has_completion_signal' "$analysis_file")
|
|
655
|
-
local loop_number=$(jq -r '.loop_number' "$analysis_file")
|
|
656
|
-
local has_progress=$(jq -r '.analysis.has_progress' "$analysis_file")
|
|
929
|
+
local is_test_only=$(jq -r -j '.analysis.is_test_only' "$analysis_file")
|
|
930
|
+
local has_completion_signal=$(jq -r -j '.analysis.has_completion_signal' "$analysis_file")
|
|
931
|
+
local loop_number=$(jq -r -j '.loop_number' "$analysis_file")
|
|
932
|
+
local has_progress=$(jq -r -j '.analysis.has_progress' "$analysis_file")
|
|
657
933
|
|
|
658
934
|
# Read current exit signals
|
|
659
935
|
local signals=$(cat "$exit_signals_file" 2>/dev/null || echo '{"test_only_loops": [], "done_signals": [], "completion_indicators": []}')
|
|
@@ -677,7 +953,7 @@ update_exit_signals() {
|
|
|
677
953
|
# Note: Previously used confidence >= 60, but JSON mode always has confidence >= 70
|
|
678
954
|
# due to deterministic scoring (+50 for JSON format, +20 for result field).
|
|
679
955
|
# This caused premature exits after 5 loops. Now we respect Claude's explicit intent.
|
|
680
|
-
local exit_signal=$(jq -r '.analysis.exit_signal // false' "$analysis_file")
|
|
956
|
+
local exit_signal=$(jq -r -j '.analysis.exit_signal // false' "$analysis_file")
|
|
681
957
|
if [[ "$exit_signal" == "true" ]]; then
|
|
682
958
|
signals=$(echo "$signals" | jq ".completion_indicators += [$loop_number]")
|
|
683
959
|
fi
|
|
@@ -701,12 +977,12 @@ log_analysis_summary() {
|
|
|
701
977
|
return 1
|
|
702
978
|
fi
|
|
703
979
|
|
|
704
|
-
local loop=$(jq -r '.loop_number' "$analysis_file")
|
|
705
|
-
local exit_sig=$(jq -r '.analysis.exit_signal' "$analysis_file")
|
|
706
|
-
local confidence=$(jq -r '.analysis.confidence_score' "$analysis_file")
|
|
707
|
-
local test_only=$(jq -r '.analysis.is_test_only' "$analysis_file")
|
|
708
|
-
local files_changed=$(jq -r '.analysis.files_modified' "$analysis_file")
|
|
709
|
-
local summary=$(jq -r '.analysis.work_summary' "$analysis_file")
|
|
980
|
+
local loop=$(jq -r -j '.loop_number' "$analysis_file")
|
|
981
|
+
local exit_sig=$(jq -r -j '.analysis.exit_signal' "$analysis_file")
|
|
982
|
+
local confidence=$(jq -r -j '.analysis.confidence_score' "$analysis_file")
|
|
983
|
+
local test_only=$(jq -r -j '.analysis.is_test_only' "$analysis_file")
|
|
984
|
+
local files_changed=$(jq -r -j '.analysis.files_modified' "$analysis_file")
|
|
985
|
+
local summary=$(jq -r -j '.analysis.work_summary' "$analysis_file")
|
|
710
986
|
|
|
711
987
|
echo -e "${BLUE}╔════════════════════════════════════════════════════════════╗${NC}"
|
|
712
988
|
echo -e "${BLUE}║ Response Analysis - Loop #$loop ║${NC}"
|
|
@@ -777,6 +1053,52 @@ SESSION_FILE="$RALPH_DIR/.claude_session_id"
|
|
|
777
1053
|
# Session expiration time in seconds (24 hours)
|
|
778
1054
|
SESSION_EXPIRATION_SECONDS=86400
|
|
779
1055
|
|
|
1056
|
+
get_session_file_age_seconds() {
|
|
1057
|
+
if [[ ! -f "$SESSION_FILE" ]]; then
|
|
1058
|
+
echo "-1"
|
|
1059
|
+
return 1
|
|
1060
|
+
fi
|
|
1061
|
+
|
|
1062
|
+
local now
|
|
1063
|
+
now=$(get_epoch_seconds)
|
|
1064
|
+
local file_time=""
|
|
1065
|
+
|
|
1066
|
+
if file_time=$(stat -c %Y "$SESSION_FILE" 2>/dev/null); then
|
|
1067
|
+
:
|
|
1068
|
+
elif file_time=$(stat -f %m "$SESSION_FILE" 2>/dev/null); then
|
|
1069
|
+
:
|
|
1070
|
+
else
|
|
1071
|
+
echo "-1"
|
|
1072
|
+
return 1
|
|
1073
|
+
fi
|
|
1074
|
+
|
|
1075
|
+
echo $((now - file_time))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
read_session_id_from_file() {
|
|
1079
|
+
if [[ ! -f "$SESSION_FILE" ]]; then
|
|
1080
|
+
echo ""
|
|
1081
|
+
return 1
|
|
1082
|
+
fi
|
|
1083
|
+
|
|
1084
|
+
local raw_content
|
|
1085
|
+
raw_content=$(cat "$SESSION_FILE" 2>/dev/null)
|
|
1086
|
+
if [[ -z "$raw_content" ]]; then
|
|
1087
|
+
echo ""
|
|
1088
|
+
return 1
|
|
1089
|
+
fi
|
|
1090
|
+
|
|
1091
|
+
local session_id=""
|
|
1092
|
+
if echo "$raw_content" | jq -e . >/dev/null 2>&1; then
|
|
1093
|
+
session_id=$(echo "$raw_content" | jq -r -j '.session_id // .sessionId // ""' 2>/dev/null)
|
|
1094
|
+
else
|
|
1095
|
+
session_id=$(printf '%s' "$raw_content" | tr -d '\r' | head -n 1)
|
|
1096
|
+
fi
|
|
1097
|
+
|
|
1098
|
+
echo "$session_id"
|
|
1099
|
+
[[ -n "$session_id" && "$session_id" != "null" ]]
|
|
1100
|
+
}
|
|
1101
|
+
|
|
780
1102
|
# Store session ID to file with timestamp
|
|
781
1103
|
# Usage: store_session_id "session-uuid-123"
|
|
782
1104
|
store_session_id() {
|
|
@@ -786,14 +1108,8 @@ store_session_id() {
|
|
|
786
1108
|
return 1
|
|
787
1109
|
fi
|
|
788
1110
|
|
|
789
|
-
#
|
|
790
|
-
|
|
791
|
-
--arg session_id "$session_id" \
|
|
792
|
-
--arg timestamp "$(get_iso_timestamp)" \
|
|
793
|
-
'{
|
|
794
|
-
session_id: $session_id,
|
|
795
|
-
timestamp: $timestamp
|
|
796
|
-
}' > "$SESSION_FILE"
|
|
1111
|
+
# Persist the session as a raw ID so the main loop can resume it directly.
|
|
1112
|
+
printf '%s\n' "$session_id" > "$SESSION_FILE"
|
|
797
1113
|
|
|
798
1114
|
return 0
|
|
799
1115
|
}
|
|
@@ -801,14 +1117,7 @@ store_session_id() {
|
|
|
801
1117
|
# Get the last stored session ID
|
|
802
1118
|
# Returns: session ID string or empty if not found
|
|
803
1119
|
get_last_session_id() {
|
|
804
|
-
|
|
805
|
-
echo ""
|
|
806
|
-
return 0
|
|
807
|
-
fi
|
|
808
|
-
|
|
809
|
-
# Extract session_id from JSON file
|
|
810
|
-
local session_id=$(jq -r '.session_id // ""' "$SESSION_FILE" 2>/dev/null)
|
|
811
|
-
echo "$session_id"
|
|
1120
|
+
read_session_id_from_file || true
|
|
812
1121
|
return 0
|
|
813
1122
|
}
|
|
814
1123
|
|
|
@@ -820,23 +1129,35 @@ should_resume_session() {
|
|
|
820
1129
|
return 1
|
|
821
1130
|
fi
|
|
822
1131
|
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if [[ -z "$timestamp" ]]; then
|
|
1132
|
+
local session_id
|
|
1133
|
+
session_id=$(read_session_id_from_file) || {
|
|
827
1134
|
echo "false"
|
|
828
1135
|
return 1
|
|
829
|
-
|
|
1136
|
+
}
|
|
830
1137
|
|
|
831
|
-
#
|
|
832
|
-
local
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1138
|
+
# Support legacy JSON session files that still carry a timestamp.
|
|
1139
|
+
local timestamp=""
|
|
1140
|
+
if jq -e . "$SESSION_FILE" >/dev/null 2>&1; then
|
|
1141
|
+
timestamp=$(jq -r -j '.timestamp // ""' "$SESSION_FILE" 2>/dev/null)
|
|
1142
|
+
fi
|
|
836
1143
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1144
|
+
local age=0
|
|
1145
|
+
if [[ -n "$timestamp" && "$timestamp" != "null" ]]; then
|
|
1146
|
+
# Calculate session age using date utilities
|
|
1147
|
+
local now
|
|
1148
|
+
now=$(get_epoch_seconds)
|
|
1149
|
+
local session_time
|
|
1150
|
+
session_time=$(parse_iso_to_epoch "$timestamp")
|
|
1151
|
+
|
|
1152
|
+
# If parse_iso_to_epoch fell back to current epoch, session_time ≈ now → age ≈ 0.
|
|
1153
|
+
# That's a safe default: treat unparseable timestamps as fresh rather than expired.
|
|
1154
|
+
age=$((now - session_time))
|
|
1155
|
+
else
|
|
1156
|
+
age=$(get_session_file_age_seconds) || {
|
|
1157
|
+
echo "false"
|
|
1158
|
+
return 1
|
|
1159
|
+
}
|
|
1160
|
+
fi
|
|
840
1161
|
|
|
841
1162
|
# Check if session is still valid (less than expiration time)
|
|
842
1163
|
if [[ $age -lt $SESSION_EXPIRATION_SECONDS ]]; then
|
|
@@ -850,6 +1171,14 @@ should_resume_session() {
|
|
|
850
1171
|
|
|
851
1172
|
# Export functions for use in ralph_loop.sh
|
|
852
1173
|
export -f detect_output_format
|
|
1174
|
+
export -f count_json_documents
|
|
1175
|
+
export -f normalize_cli_array_response
|
|
1176
|
+
export -f normalize_codex_jsonl_response
|
|
1177
|
+
export -f normalize_cursor_stream_json_response
|
|
1178
|
+
export -f is_codex_jsonl_output
|
|
1179
|
+
export -f is_cursor_stream_json_output
|
|
1180
|
+
export -f normalize_json_output
|
|
1181
|
+
export -f extract_session_id_from_output
|
|
853
1182
|
export -f parse_json_response
|
|
854
1183
|
export -f analyze_response
|
|
855
1184
|
export -f update_exit_signals
|