bmalph 2.7.4 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.7.4",
3
+ "version": "2.7.5",
4
4
  "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
5
5
  "type": "module",
6
6
  "bin": {
@@ -85,13 +85,19 @@ Ralph maintains session continuity across loop iterations using `--resume` with
85
85
 
86
86
  Ralph uses `--resume <session_id>` instead of `--continue` to resume sessions. This ensures Ralph only resumes its own saved sessions and avoids hijacking unrelated active sessions.
87
87
 
88
+ This applies to every driver that exposes resumable IDs today:
89
+
90
+ - Claude Code
91
+ - OpenAI Codex
92
+ - Cursor
93
+
88
94
  ### Session Files
89
95
 
90
96
  | File | Purpose |
91
97
  |------|---------|
92
98
  | `.ralph/.ralph_session` | Current session ID and timestamps |
93
99
  | `.ralph/.ralph_session_history` | History of last 50 session transitions |
94
- | `.ralph/.claude_session_id` | Persisted driver session ID (shared filename for historical reasons) |
100
+ | `.ralph/.claude_session_id` | Persisted driver session ID (shared filename for historical reasons; used by Claude Code, Codex, and Cursor) |
95
101
 
96
102
  ### Session Lifecycle
97
103
 
@@ -256,7 +262,9 @@ bash .ralph/ralph_loop.sh --monitor --live # Live streaming with tmux monitoring
256
262
 
257
263
  ### How It Works
258
264
 
259
- - Switches output format to `stream-json` and pipes through `jq` for human-readable display
265
+ - Live mode switches the active driver to its structured streaming format and pipes the stream through `jq`
266
+ - Cursor background loop execution stays on `json` output and switches to `stream-json` for live display
267
+ - Claude Code also uses `stream-json` for live display, while Codex streams its native JSONL events directly
260
268
  - Shows text deltas and tool invocations in real-time
261
269
  - Requires `jq` and `stdbuf` (from coreutils); falls back to background mode if unavailable
262
270
 
@@ -341,6 +349,20 @@ When using `--monitor` with `--live`, tmux creates a 3-pane layout:
341
349
  2. Start a new session with `bash .ralph/ralph_loop.sh --reset-session`
342
350
  3. Context will be rebuilt from `@fix_plan.md` and `specs/`
343
351
 
352
+ #### Cursor preflight fails
353
+
354
+ **Symptoms:** `bmalph doctor` or `bmalph run --driver cursor` fails before the loop starts
355
+
356
+ **Causes:**
357
+ - `command -v jq` fails in the bash environment Ralph uses
358
+ - `command -v cursor-agent` fails in that same bash environment
359
+ - `cursor-agent status` reports an authentication problem
360
+
361
+ **Solutions:**
362
+ 1. Run `command -v jq` in the same bash shell Ralph uses and install `jq` if missing
363
+ 2. Run `command -v cursor-agent` and ensure the official Cursor CLI is on the bash `PATH`
364
+ 3. Run `cursor-agent status` and sign in to Cursor before starting Ralph
365
+
344
366
  ### Diagnostic Commands
345
367
 
346
368
  ```bash
@@ -383,12 +405,14 @@ Loop execution logs are stored in `.ralph/logs/`:
383
405
  "calls_made_this_hour": 25,
384
406
  "max_calls_per_hour": 100,
385
407
  "last_action": "description",
386
- "status": "running|paused|complete",
408
+ "status": "running|completed|halted|paused|stopped|success|graceful_exit|error",
387
409
  "exit_reason": "reason (if exited)",
388
410
  "next_reset": "timestamp for rate limit reset"
389
411
  }
390
412
  ```
391
413
 
414
+ `bmalph status` normalizes these raw bash values to `running`, `blocked`, `completed`, `not_started`, or `unknown`.
415
+
392
416
  ---
393
417
 
394
418
  ## Error Detection
@@ -1,11 +1,7 @@
1
1
  #!/bin/bash
2
- # Cursor CLI driver for Ralph (EXPERIMENTAL)
3
- # Provides platform-specific CLI invocation logic for Cursor CLI.
4
- #
5
- # Known limitations:
6
- # - CLI is in beta — binary name and flags may change
7
- # - NDJSON stream format assumes {type: "text", content: "..."} events
8
- # - Session continuity is disabled until Cursor exposes a stable capturable session ID
2
+ # Cursor CLI driver for Ralph
3
+ # Uses the documented cursor-agent contract for background execution and
4
+ # switches to stream-json only for live display paths.
9
5
 
10
6
  driver_name() {
11
7
  echo "cursor"
@@ -42,7 +38,6 @@ driver_check_available() {
42
38
  command -v "$cli_binary" &>/dev/null
43
39
  }
44
40
 
45
- # Cursor CLI tool names
46
41
  driver_valid_tools() {
47
42
  VALID_TOOL_PATTERNS=(
48
43
  "file_edit"
@@ -53,10 +48,6 @@ driver_valid_tools() {
53
48
  )
54
49
  }
55
50
 
56
- # Build Cursor CLI command
57
- # Context is prepended to the prompt (same pattern as Codex/Copilot drivers).
58
- # Uses --print for headless mode, --force for autonomous execution,
59
- # --output-format stream-json for NDJSON streaming.
60
51
  driver_build_command() {
61
52
  local prompt_file=$1
62
53
  local loop_context=$2
@@ -76,16 +67,12 @@ driver_build_command() {
76
67
  CLAUDE_CMD_ARGS+=("$cli_binary")
77
68
  fi
78
69
 
79
- # Headless mode
80
- CLAUDE_CMD_ARGS+=("--print")
70
+ CLAUDE_CMD_ARGS+=("-p" "--force" "--output-format" "json")
81
71
 
82
- # Autonomous execution
83
- CLAUDE_CMD_ARGS+=("--force")
84
-
85
- # NDJSON streaming output
86
- CLAUDE_CMD_ARGS+=("--output-format" "stream-json")
72
+ if [[ "$CLAUDE_USE_CONTINUE" == "true" && -n "$session_id" ]]; then
73
+ CLAUDE_CMD_ARGS+=("--resume" "$session_id")
74
+ fi
87
75
 
88
- # Build prompt with context prepended
89
76
  local prompt_content
90
77
  if driver_running_on_windows; then
91
78
  prompt_content=$(driver_build_windows_bootstrap_prompt "$loop_context" "$prompt_file")
@@ -102,20 +89,43 @@ $prompt_content"
102
89
  }
103
90
 
104
91
  driver_supports_sessions() {
105
- return 1 # false — session IDs are not capturable from current NDJSON output
92
+ return 0
106
93
  }
107
94
 
108
95
  driver_supports_live_output() {
109
- return 0 # true
96
+ return 0
110
97
  }
111
98
 
112
99
  driver_prepare_live_command() {
113
- LIVE_CMD_ARGS=("${CLAUDE_CMD_ARGS[@]}")
100
+ LIVE_CMD_ARGS=()
101
+ local skip_next=false
102
+
103
+ for arg in "${CLAUDE_CMD_ARGS[@]}"; do
104
+ if [[ "$skip_next" == "true" ]]; then
105
+ LIVE_CMD_ARGS+=("stream-json")
106
+ skip_next=false
107
+ elif [[ "$arg" == "--output-format" ]]; then
108
+ LIVE_CMD_ARGS+=("$arg")
109
+ skip_next=true
110
+ else
111
+ LIVE_CMD_ARGS+=("$arg")
112
+ fi
113
+ done
114
+
115
+ if [[ "$skip_next" == "true" ]]; then
116
+ return 1
117
+ fi
114
118
  }
115
119
 
116
- # Cursor CLI outputs NDJSON events
117
120
  driver_stream_filter() {
118
- echo 'select(.type == "text") | .content // empty'
121
+ echo '
122
+ if .type == "assistant" then
123
+ [(.message.content[]? | select(.type == "text") | .text)] | join("\n")
124
+ elif .type == "tool_call" then
125
+ "\n\n⚡ [" + (.tool_call.name // .name // "tool_call") + "]\n"
126
+ else
127
+ empty
128
+ end'
119
129
  }
120
130
 
121
131
  driver_running_on_windows() {
@@ -208,10 +218,18 @@ driver_localappdata_cli_binary() {
208
218
  local_app_data=$(cygpath -u "$local_app_data")
209
219
  fi
210
220
 
211
- local candidate="$local_app_data/cursor-agent/agent.cmd"
212
- if [[ -f "$candidate" ]]; then
213
- echo "$candidate"
214
- fi
221
+ local candidates=(
222
+ "$local_app_data/cursor-agent/cursor-agent.cmd"
223
+ "$local_app_data/cursor-agent/agent.cmd"
224
+ )
225
+
226
+ local candidate
227
+ for candidate in "${candidates[@]}"; do
228
+ if [[ -f "$candidate" ]]; then
229
+ echo "$candidate"
230
+ return 0
231
+ fi
232
+ done
215
233
  }
216
234
 
217
235
  driver_wrapper_path() {
@@ -97,6 +97,37 @@ normalize_codex_jsonl_response() {
97
97
  ' "$output_file" > "$normalized_file"
98
98
  }
99
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
+
100
131
  # Detect whether a multi-document stream matches Codex JSONL events.
101
132
  is_codex_jsonl_output() {
102
133
  local output_file=$1
@@ -112,6 +143,24 @@ is_codex_jsonl_output() {
112
143
  ' < "$output_file" 2>/dev/null
113
144
  }
114
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
+
115
164
  # Normalize structured output to a single object when downstream parsing expects one.
116
165
  normalize_json_output() {
117
166
  local output_file=$1
@@ -129,6 +178,14 @@ normalize_json_output() {
129
178
  return $?
130
179
  fi
131
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
+
132
189
  return 1
133
190
  fi
134
191
 
@@ -215,25 +272,45 @@ detect_output_format() {
215
272
 
216
273
  if [[ "$is_codex_jsonl" == "true" ]]; then
217
274
  echo "json"
218
- else
275
+ return
276
+ fi
277
+
278
+ local is_cursor_stream_json
279
+ is_cursor_stream_json=$(is_cursor_stream_json_output "$output_file") || {
219
280
  echo "text"
281
+ return
282
+ }
283
+
284
+ if [[ "$is_cursor_stream_json" == "true" ]]; then
285
+ echo "json"
286
+ return
220
287
  fi
221
- return
222
288
  fi
223
289
 
224
290
  echo "text"
225
291
  }
226
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"
300
+ }
301
+
227
302
  # Parse JSON response and extract structured fields
228
303
  # Creates .ralph/.json_parse_result with normalized analysis data
229
- # Supports FOUR JSON formats:
304
+ # Supports FIVE JSON formats:
230
305
  # 1. Flat format: { status, exit_signal, work_type, files_modified, ... }
231
306
  # 2. Claude CLI object format: { result, sessionId, metadata: { files_changed, has_errors, completion_status, ... } }
232
307
  # 3. Claude CLI array format: [ {type: "system", ...}, {type: "assistant", ...}, {type: "result", ...} ]
233
308
  # 4. Codex JSONL format: {"type":"thread.started",...}\n{"type":"item.completed","item":{...}}
309
+ # 5. Cursor stream-json format: {"type":"assistant",...}\n{"type":"result",...}
234
310
  parse_json_response() {
235
311
  local output_file=$1
236
312
  local result_file="${2:-$RALPH_DIR/.json_parse_result}"
313
+ local original_output_file=$output_file
237
314
  local normalized_file=""
238
315
  local json_document_count=""
239
316
  local response_shape="object"
@@ -267,7 +344,11 @@ parse_json_response() {
267
344
  output_file="$normalized_file"
268
345
 
269
346
  if [[ "$response_shape" == "jsonl" ]]; then
270
- response_shape="codex_jsonl"
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
271
352
  fi
272
353
  fi
273
354
 
@@ -296,7 +377,7 @@ parse_json_response() {
296
377
  if [[ -n "$result_text" ]] && echo "$result_text" | grep -q -- "---RALPH_STATUS---"; then
297
378
  # Extract EXIT_SIGNAL value from RALPH_STATUS block within result text
298
379
  local embedded_exit_sig
299
- embedded_exit_sig=$(echo "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2 | tr -d '\r' | xargs)
380
+ embedded_exit_sig=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "EXIT_SIGNAL:" | cut -d: -f2)")
300
381
  if [[ -n "$embedded_exit_sig" ]]; then
301
382
  # Explicit EXIT_SIGNAL found in RALPH_STATUS block
302
383
  explicit_exit_signal_found="true"
@@ -311,7 +392,7 @@ parse_json_response() {
311
392
  # Also check STATUS field as fallback ONLY when EXIT_SIGNAL was not specified
312
393
  # This respects explicit EXIT_SIGNAL: false which means "task complete, continue working"
313
394
  local embedded_status
314
- embedded_status=$(echo "$result_text" | grep "STATUS:" | cut -d: -f2 | tr -d '\r' | xargs)
395
+ embedded_status=$(trim_shell_whitespace "$(printf '%s\n' "$result_text" | grep "STATUS:" | cut -d: -f2)")
315
396
  if [[ "$embedded_status" == "COMPLETE" && "$explicit_exit_signal_found" != "true" ]]; then
316
397
  # STATUS: COMPLETE without any EXIT_SIGNAL field implies completion
317
398
  exit_signal="true"
@@ -341,7 +422,7 @@ parse_json_response() {
341
422
  local summary=$(jq -r -j '.result // .summary // ""' "$output_file" 2>/dev/null)
342
423
 
343
424
  # Session ID: from Claude CLI format (sessionId) OR from metadata.session_id
344
- local session_id=$(jq -r -j '.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)
345
426
 
346
427
  # Loop number: from metadata
347
428
  local loop_number=$(jq -r -j '.metadata.loop_number // .loop_number // 0' "$output_file" 2>/dev/null)
@@ -375,7 +456,7 @@ parse_json_response() {
375
456
  # completion markers are absent. This keeps JSONL analysis aligned with text mode.
376
457
  local summary_has_completion_keyword="false"
377
458
  local summary_has_no_work_pattern="false"
378
- if [[ "$response_shape" == "codex_jsonl" && "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
459
+ if [[ "$response_shape" == "codex_jsonl" || "$response_shape" == "cursor_stream_jsonl" ]] && [[ "$explicit_exit_signal_found" != "true" && -n "$summary" ]]; then
379
460
  for keyword in "${COMPLETION_KEYWORDS[@]}"; do
380
461
  if echo "$summary" | grep -qi "$keyword"; then
381
462
  summary_has_completion_keyword="true"
@@ -639,8 +720,8 @@ analyze_response() {
639
720
  # 1. Check for explicit structured output (if Claude follows schema)
640
721
  if grep -q -- "---RALPH_STATUS---" "$output_file"; then
641
722
  # Parse structured output
642
- local status=$(grep "STATUS:" "$output_file" | cut -d: -f2 | tr -d '\r' | xargs)
643
- local exit_sig=$(grep "EXIT_SIGNAL:" "$output_file" | cut -d: -f2 | tr -d '\r' | xargs)
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)")
644
725
 
645
726
  # If EXIT_SIGNAL is explicitly provided, respect it
646
727
  if [[ -n "$exit_sig" ]]; then
@@ -1093,7 +1174,9 @@ export -f detect_output_format
1093
1174
  export -f count_json_documents
1094
1175
  export -f normalize_cli_array_response
1095
1176
  export -f normalize_codex_jsonl_response
1177
+ export -f normalize_cursor_stream_json_response
1096
1178
  export -f is_codex_jsonl_output
1179
+ export -f is_cursor_stream_json_output
1097
1180
  export -f normalize_json_output
1098
1181
  export -f extract_session_id_from_output
1099
1182
  export -f parse_json_response