bmalph 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +105 -38
  2. package/dist/cli.js +19 -0
  3. package/dist/commands/doctor.d.ts +0 -11
  4. package/dist/commands/doctor.js +22 -55
  5. package/dist/commands/implement.d.ts +6 -0
  6. package/dist/commands/implement.js +82 -0
  7. package/dist/commands/init.js +4 -2
  8. package/dist/commands/reset.d.ts +7 -0
  9. package/dist/commands/reset.js +81 -0
  10. package/dist/commands/status.js +100 -11
  11. package/dist/commands/watch.d.ts +6 -0
  12. package/dist/commands/watch.js +19 -0
  13. package/dist/installer.d.ts +0 -6
  14. package/dist/installer.js +0 -10
  15. package/dist/platform/claude-code.js +0 -1
  16. package/dist/reset.d.ts +18 -0
  17. package/dist/reset.js +181 -0
  18. package/dist/transition/artifact-scan.d.ts +27 -0
  19. package/dist/transition/artifact-scan.js +91 -0
  20. package/dist/transition/artifacts.d.ts +0 -1
  21. package/dist/transition/artifacts.js +0 -26
  22. package/dist/transition/context.js +34 -0
  23. package/dist/transition/fix-plan.d.ts +8 -2
  24. package/dist/transition/fix-plan.js +33 -7
  25. package/dist/transition/index.d.ts +1 -1
  26. package/dist/transition/index.js +1 -1
  27. package/dist/transition/orchestration.d.ts +2 -2
  28. package/dist/transition/orchestration.js +120 -41
  29. package/dist/transition/preflight.d.ts +6 -0
  30. package/dist/transition/preflight.js +154 -0
  31. package/dist/transition/specs-index.d.ts +1 -1
  32. package/dist/transition/specs-index.js +24 -1
  33. package/dist/transition/types.d.ts +23 -1
  34. package/dist/utils/dryrun.d.ts +1 -1
  35. package/dist/utils/dryrun.js +22 -0
  36. package/dist/utils/state.d.ts +0 -2
  37. package/dist/utils/validate.js +3 -2
  38. package/dist/watch/dashboard.d.ts +4 -0
  39. package/dist/watch/dashboard.js +60 -0
  40. package/dist/watch/file-watcher.d.ts +9 -0
  41. package/dist/watch/file-watcher.js +27 -0
  42. package/dist/watch/renderer.d.ts +16 -0
  43. package/dist/watch/renderer.js +241 -0
  44. package/dist/watch/state-reader.d.ts +9 -0
  45. package/dist/watch/state-reader.js +190 -0
  46. package/dist/watch/types.d.ts +55 -0
  47. package/dist/watch/types.js +1 -0
  48. package/package.json +9 -4
  49. package/ralph/lib/circuit_breaker.sh +86 -59
  50. package/ralph/lib/enable_core.sh +3 -6
  51. package/ralph/lib/response_analyzer.sh +5 -29
  52. package/ralph/lib/task_sources.sh +45 -11
  53. package/ralph/lib/wizard_utils.sh +9 -0
  54. package/ralph/ralph_import.sh +7 -2
  55. package/ralph/ralph_loop.sh +44 -34
  56. package/ralph/ralph_monitor.sh +4 -0
  57. package/slash-commands/bmalph-doctor.md +16 -0
  58. package/slash-commands/bmalph-implement.md +18 -141
  59. package/slash-commands/bmalph-status.md +15 -0
  60. package/slash-commands/bmalph-upgrade.md +15 -0
  61. package/slash-commands/bmalph-watch.md +20 -0
@@ -0,0 +1,55 @@
1
+ export interface LoopInfo {
2
+ loopCount: number;
3
+ status: string;
4
+ lastAction: string;
5
+ callsMadeThisHour: number;
6
+ maxCallsPerHour: number;
7
+ }
8
+ export interface CircuitBreakerInfo {
9
+ state: "CLOSED" | "HALF_OPEN" | "OPEN";
10
+ consecutiveNoProgress: number;
11
+ totalOpens: number;
12
+ reason?: string;
13
+ }
14
+ export interface StoryProgress {
15
+ completed: number;
16
+ total: number;
17
+ nextStory: string | null;
18
+ }
19
+ export interface AnalysisInfo {
20
+ filesModified: number;
21
+ confidenceScore: number;
22
+ isTestOnly: boolean;
23
+ isStuck: boolean;
24
+ exitSignal: boolean;
25
+ hasPermissionDenials: boolean;
26
+ permissionDenialCount: number;
27
+ }
28
+ export interface ExecutionProgress {
29
+ status: "executing" | "idle";
30
+ elapsedSeconds: number;
31
+ }
32
+ export interface SessionInfo {
33
+ createdAt: string;
34
+ lastUsed?: string;
35
+ }
36
+ export interface LogEntry {
37
+ timestamp: string;
38
+ level: string;
39
+ message: string;
40
+ }
41
+ export interface DashboardState {
42
+ loop: LoopInfo | null;
43
+ circuitBreaker: CircuitBreakerInfo | null;
44
+ stories: StoryProgress | null;
45
+ analysis: AnalysisInfo | null;
46
+ execution: ExecutionProgress | null;
47
+ session: SessionInfo | null;
48
+ recentLogs: LogEntry[];
49
+ ralphCompleted: boolean;
50
+ lastUpdated: Date;
51
+ }
52
+ export interface WatchOptions {
53
+ interval: number;
54
+ projectDir: string;
55
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bmalph",
3
- "version": "2.3.0",
4
- "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop for Claude Code",
3
+ "version": "2.5.0",
4
+ "description": "Unified AI Development Framework - BMAD phases with Ralph execution loop",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "bmalph": "./bin/bmalph.js"
@@ -13,7 +13,8 @@
13
13
  "test:watch": "vitest",
14
14
  "test:e2e": "vitest run --config vitest.config.e2e.ts",
15
15
  "test:coverage": "vitest run --coverage",
16
- "test:all": "npm run test && npm run test:e2e",
16
+ "test:bash": "bash -c 'command -v bats &>/dev/null && bats tests/bash/*.bats tests/bash/drivers/*.bats || echo \"[skip] bats not installed\"'",
17
+ "test:all": "npm run test && npm run test:e2e && npm run test:bash",
17
18
  "check": "npm run lint && npm run build && npm test",
18
19
  "dev": "tsc --watch",
19
20
  "lint": "eslint src tests",
@@ -31,7 +32,11 @@
31
32
  "ai",
32
33
  "development",
33
34
  "framework",
34
- "agents"
35
+ "agents",
36
+ "bmad",
37
+ "ralph",
38
+ "autonomous",
39
+ "coding-assistant"
35
40
  ],
36
41
  "author": "Lars Cowe",
37
42
  "license": "MIT",
@@ -43,18 +43,19 @@ init_circuit_breaker() {
43
43
  fi
44
44
 
45
45
  if [[ ! -f "$CB_STATE_FILE" ]]; then
46
- cat > "$CB_STATE_FILE" << EOF
47
- {
48
- "state": "$CB_STATE_CLOSED",
49
- "last_change": "$(get_iso_timestamp)",
50
- "consecutive_no_progress": 0,
51
- "consecutive_same_error": 0,
52
- "consecutive_permission_denials": 0,
53
- "last_progress_loop": 0,
54
- "total_opens": 0,
55
- "reason": ""
56
- }
57
- EOF
46
+ jq -n \
47
+ --arg state "$CB_STATE_CLOSED" \
48
+ --arg last_change "$(get_iso_timestamp)" \
49
+ '{
50
+ state: $state,
51
+ last_change: $last_change,
52
+ consecutive_no_progress: 0,
53
+ consecutive_same_error: 0,
54
+ consecutive_permission_denials: 0,
55
+ last_progress_loop: 0,
56
+ total_opens: 0,
57
+ reason: ""
58
+ }' > "$CB_STATE_FILE"
58
59
  fi
59
60
 
60
61
  # Ensure history file exists before any transition logging
@@ -81,18 +82,20 @@ EOF
81
82
  total_opens=$(jq -r '.total_opens // 0' "$CB_STATE_FILE" 2>/dev/null || echo "0")
82
83
  log_circuit_transition "$CB_STATE_OPEN" "$CB_STATE_CLOSED" "Auto-reset on startup (CB_AUTO_RESET=true)" "$current_loop"
83
84
 
84
- cat > "$CB_STATE_FILE" << EOF
85
- {
86
- "state": "$CB_STATE_CLOSED",
87
- "last_change": "$(get_iso_timestamp)",
88
- "consecutive_no_progress": 0,
89
- "consecutive_same_error": 0,
90
- "consecutive_permission_denials": 0,
91
- "last_progress_loop": 0,
92
- "total_opens": $total_opens,
93
- "reason": "Auto-reset on startup"
94
- }
95
- EOF
85
+ jq -n \
86
+ --arg state "$CB_STATE_CLOSED" \
87
+ --arg last_change "$(get_iso_timestamp)" \
88
+ --argjson total_opens "$total_opens" \
89
+ '{
90
+ state: $state,
91
+ last_change: $last_change,
92
+ consecutive_no_progress: 0,
93
+ consecutive_same_error: 0,
94
+ consecutive_permission_denials: 0,
95
+ last_progress_loop: 0,
96
+ total_opens: $total_opens,
97
+ reason: "Auto-reset on startup"
98
+ }' > "$CB_STATE_FILE"
96
99
  else
97
100
  # Cooldown: check if enough time has elapsed to transition to HALF_OPEN
98
101
  local opened_at
@@ -296,20 +299,34 @@ record_loop_result() {
296
299
  opened_at=$(echo "$state_data" | jq -r '.opened_at // .last_change // ""' 2>/dev/null)
297
300
  fi
298
301
 
299
- cat > "$CB_STATE_FILE" << EOF
300
- {
301
- "state": "$new_state",
302
- "last_change": "$(get_iso_timestamp)",
303
- "consecutive_no_progress": $consecutive_no_progress,
304
- "consecutive_same_error": $consecutive_same_error,
305
- "consecutive_permission_denials": $consecutive_permission_denials,
306
- "last_progress_loop": $last_progress_loop,
307
- "total_opens": $total_opens,
308
- "reason": "$reason",
309
- "current_loop": $loop_number$(if [[ -n "$opened_at" ]]; then echo ",
310
- \"opened_at\": \"$opened_at\""; fi)
311
- }
312
- EOF
302
+ jq -n \
303
+ --arg state "$new_state" \
304
+ --arg last_change "$(get_iso_timestamp)" \
305
+ --argjson consecutive_no_progress "$consecutive_no_progress" \
306
+ --argjson consecutive_same_error "$consecutive_same_error" \
307
+ --argjson consecutive_permission_denials "$consecutive_permission_denials" \
308
+ --argjson last_progress_loop "$last_progress_loop" \
309
+ --argjson total_opens "$total_opens" \
310
+ --arg reason "$reason" \
311
+ --argjson current_loop "$loop_number" \
312
+ '{
313
+ state: $state,
314
+ last_change: $last_change,
315
+ consecutive_no_progress: $consecutive_no_progress,
316
+ consecutive_same_error: $consecutive_same_error,
317
+ consecutive_permission_denials: $consecutive_permission_denials,
318
+ last_progress_loop: $last_progress_loop,
319
+ total_opens: $total_opens,
320
+ reason: $reason,
321
+ current_loop: $current_loop
322
+ }' > "$CB_STATE_FILE"
323
+
324
+ # Add opened_at if set (entering or staying in OPEN state)
325
+ if [[ -n "$opened_at" ]]; then
326
+ local tmp
327
+ tmp=$(jq --arg opened_at "$opened_at" '. + {opened_at: $opened_at}' "$CB_STATE_FILE")
328
+ echo "$tmp" > "$CB_STATE_FILE"
329
+ fi
313
330
 
314
331
  # Log state transition
315
332
  if [[ "$new_state" != "$current_state" ]]; then
@@ -331,15 +348,23 @@ log_circuit_transition() {
331
348
  local reason=$3
332
349
  local loop_number=$4
333
350
 
334
- local history=$(cat "$CB_HISTORY_FILE")
335
- local transition="{
336
- \"timestamp\": \"$(get_iso_timestamp)\",
337
- \"loop\": $loop_number,
338
- \"from_state\": \"$from_state\",
339
- \"to_state\": \"$to_state\",
340
- \"reason\": \"$reason\"
341
- }"
342
-
351
+ local transition
352
+ transition=$(jq -n -c \
353
+ --arg timestamp "$(get_iso_timestamp)" \
354
+ --argjson loop "$loop_number" \
355
+ --arg from_state "$from_state" \
356
+ --arg to_state "$to_state" \
357
+ --arg reason "$reason" \
358
+ '{
359
+ timestamp: $timestamp,
360
+ loop: $loop,
361
+ from_state: $from_state,
362
+ to_state: $to_state,
363
+ reason: $reason
364
+ }')
365
+
366
+ local history
367
+ history=$(cat "$CB_HISTORY_FILE")
343
368
  history=$(echo "$history" | jq ". += [$transition]")
344
369
  echo "$history" > "$CB_HISTORY_FILE"
345
370
 
@@ -406,18 +431,20 @@ show_circuit_status() {
406
431
  reset_circuit_breaker() {
407
432
  local reason=${1:-"Manual reset"}
408
433
 
409
- cat > "$CB_STATE_FILE" << EOF
410
- {
411
- "state": "$CB_STATE_CLOSED",
412
- "last_change": "$(get_iso_timestamp)",
413
- "consecutive_no_progress": 0,
414
- "consecutive_same_error": 0,
415
- "consecutive_permission_denials": 0,
416
- "last_progress_loop": 0,
417
- "total_opens": 0,
418
- "reason": "$reason"
419
- }
420
- EOF
434
+ jq -n \
435
+ --arg state "$CB_STATE_CLOSED" \
436
+ --arg last_change "$(get_iso_timestamp)" \
437
+ --arg reason "$reason" \
438
+ '{
439
+ state: $state,
440
+ last_change: $last_change,
441
+ consecutive_no_progress: 0,
442
+ consecutive_same_error: 0,
443
+ consecutive_permission_denials: 0,
444
+ last_progress_loop: 0,
445
+ total_opens: 0,
446
+ reason: $reason
447
+ }' > "$CB_STATE_FILE"
421
448
 
422
449
  echo -e "${GREEN}✅ Circuit breaker reset to CLOSED state${NC}"
423
450
  }
@@ -328,10 +328,9 @@ detect_project_context() {
328
328
  DETECTED_TEST_CMD="pnpm test"
329
329
  DETECTED_RUN_CMD="pnpm start"
330
330
  fi
331
- fi
332
331
 
333
332
  # Detect from pyproject.toml or setup.py (Python)
334
- if [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
333
+ elif [[ -f "pyproject.toml" ]] || [[ -f "setup.py" ]]; then
335
334
  DETECTED_PROJECT_TYPE="python"
336
335
 
337
336
  # Extract project name from pyproject.toml
@@ -358,19 +357,17 @@ detect_project_context() {
358
357
  DETECTED_TEST_CMD="pytest"
359
358
  DETECTED_RUN_CMD="python -m ${DETECTED_PROJECT_NAME:-main}"
360
359
  fi
361
- fi
362
360
 
363
361
  # Detect from Cargo.toml (Rust)
364
- if [[ -f "Cargo.toml" ]]; then
362
+ elif [[ -f "Cargo.toml" ]]; then
365
363
  DETECTED_PROJECT_TYPE="rust"
366
364
  DETECTED_PROJECT_NAME=$(grep -m1 '^name' Cargo.toml | sed 's/.*= *"\([^"]*\)".*/\1/' 2>/dev/null)
367
365
  DETECTED_BUILD_CMD="cargo build"
368
366
  DETECTED_TEST_CMD="cargo test"
369
367
  DETECTED_RUN_CMD="cargo run"
370
- fi
371
368
 
372
369
  # Detect from go.mod (Go)
373
- if [[ -f "go.mod" ]]; then
370
+ elif [[ -f "go.mod" ]]; then
374
371
  DETECTED_PROJECT_TYPE="go"
375
372
  DETECTED_PROJECT_NAME=$(head -1 go.mod | sed 's/module //' 2>/dev/null)
376
373
  DETECTED_BUILD_CMD="go build"
@@ -829,37 +829,13 @@ should_resume_session() {
829
829
  fi
830
830
 
831
831
  # Calculate session age using date utilities
832
- local now=$(get_epoch_seconds)
832
+ local now
833
+ now=$(get_epoch_seconds)
833
834
  local session_time
835
+ session_time=$(parse_iso_to_epoch "$timestamp")
834
836
 
835
- # Parse ISO timestamp to epoch - try multiple formats for cross-platform compatibility
836
- # Strip milliseconds if present (e.g., 2026-01-09T10:30:00.123+00:00 2026-01-09T10:30:00+00:00)
837
- local clean_timestamp="${timestamp}"
838
- if [[ "$timestamp" =~ \.[0-9]+[+-Z] ]]; then
839
- clean_timestamp=$(echo "$timestamp" | sed 's/\.[0-9]*\([+-Z]\)/\1/')
840
- fi
841
-
842
- if command -v gdate &>/dev/null; then
843
- # macOS with coreutils
844
- session_time=$(gdate -d "$clean_timestamp" +%s 2>/dev/null)
845
- elif date --version 2>&1 | grep -q GNU; then
846
- # GNU date (Linux)
847
- session_time=$(date -d "$clean_timestamp" +%s 2>/dev/null)
848
- else
849
- # BSD date (macOS without coreutils) - try parsing ISO format
850
- # Format: 2026-01-09T10:30:00+00:00 or 2026-01-09T10:30:00Z
851
- # Strip timezone suffix for BSD date parsing
852
- local date_only="${clean_timestamp%[+-Z]*}"
853
- session_time=$(date -j -f "%Y-%m-%dT%H:%M:%S" "$date_only" +%s 2>/dev/null)
854
- fi
855
-
856
- # If we couldn't parse the timestamp, consider session expired
857
- if [[ -z "$session_time" || ! "$session_time" =~ ^[0-9]+$ ]]; then
858
- echo "false"
859
- return 1
860
- fi
861
-
862
- # Calculate age in seconds
837
+ # If parse_iso_to_epoch fell back to current epoch, session_time now age 0.
838
+ # That's a safe default: treat unparseable timestamps as fresh rather than expired.
863
839
  local age=$((now - session_time))
864
840
 
865
841
  # Check if session is still valid (less than expiration time)
@@ -308,21 +308,55 @@ extract_prd_tasks() {
308
308
  done <<< "$numbered_tasks"
309
309
  fi
310
310
 
311
- # Look for headings that might be task sections
312
- local headings
313
- headings=$(grep -E '^#{1,3}[[:space:]]+(TODO|Tasks|Requirements|Features|Backlog|Sprint)' "$prd_file" 2>/dev/null)
314
- if [[ -n "$headings" ]]; then
315
- # Extract content after these headings as potential tasks
316
- while IFS= read -r heading; do
317
- local section_name
318
- section_name=$(echo "$heading" | sed -E 's/^#*[[:space:]]*//')
319
- # This is informational - actual task extraction would need more context
320
- done <<< "$headings"
311
+ # Look for headings that might be task sections (with line numbers to handle duplicates)
312
+ local heading_lines
313
+ heading_lines=$(grep -nE '^#{1,3}[[:space:]]+(TODO|Tasks|Requirements|Features|Backlog|Sprint)' "$prd_file" 2>/dev/null)
314
+ if [[ -n "$heading_lines" ]]; then
315
+ # Extract bullet items beneath each matching heading
316
+ while IFS= read -r heading_entry; do
317
+ # Parse line number directly from grep -n output (avoids duplicate heading issue)
318
+ local heading_line
319
+ heading_line=$(echo "$heading_entry" | cut -d: -f1)
320
+ [[ -z "$heading_line" ]] && continue
321
+
322
+ # Find the next heading (any level) after this one
323
+ local next_heading_line
324
+ next_heading_line=$(tail -n +"$((heading_line + 1))" "$prd_file" | grep -n '^#' | head -1 | cut -d: -f1)
325
+
326
+ # Extract the section content
327
+ local section_content
328
+ if [[ -n "$next_heading_line" ]]; then
329
+ local end_line=$((heading_line + next_heading_line - 1))
330
+ section_content=$(sed -n "$((heading_line + 1)),${end_line}p" "$prd_file")
331
+ else
332
+ section_content=$(tail -n +"$((heading_line + 1))" "$prd_file")
333
+ fi
334
+
335
+ # Extract bullet items from section and convert to checkboxes
336
+ while IFS= read -r line; do
337
+ local task_text=""
338
+ # Match "- item" or "* item" (but not checkboxes, already handled above)
339
+ if [[ "$line" =~ ^[[:space:]]*[-*][[:space:]]+(.+)$ ]]; then
340
+ task_text="${BASH_REMATCH[1]}"
341
+ # Skip checkbox lines — they are handled by the earlier extraction
342
+ if [[ "$line" == *"["*"]"* ]]; then
343
+ task_text=""
344
+ fi
345
+ # Match "N. item" numbered patterns
346
+ elif [[ "$line" =~ ^[[:space:]]*[0-9]+\.[[:space:]]+(.+)$ ]]; then
347
+ task_text="${BASH_REMATCH[1]}"
348
+ fi
349
+ if [[ -n "$task_text" ]]; then
350
+ tasks="${tasks}
351
+ - [ ] ${task_text}"
352
+ fi
353
+ done <<< "$section_content"
354
+ done <<< "$heading_lines"
321
355
  fi
322
356
 
323
357
  # Clean up and output
324
358
  if [[ -n "$tasks" ]]; then
325
- echo "$tasks" | grep -v '^$' | head -30 # Limit to 30 tasks
359
+ echo "$tasks" | grep -v '^$' | awk '!seen[$0]++' | head -30 # Deduplicate, limit to 30
326
360
  return 0
327
361
  fi
328
362
 
@@ -475,6 +475,15 @@ show_progress() {
475
475
  local total=$2
476
476
  local message=$3
477
477
 
478
+ # Guard against division by zero
479
+ if [[ $total -le 0 ]]; then
480
+ local bar_width=30
481
+ local bar=""
482
+ for ((i = 0; i < bar_width; i++)); do bar+="░"; done
483
+ echo -en "\r${WIZARD_CYAN}[${bar}]${WIZARD_NC} 0/${total} ${message}"
484
+ return 0
485
+ fi
486
+
478
487
  local bar_width=30
479
488
  local filled=$((current * bar_width / total))
480
489
  local empty=$((bar_width - filled))
@@ -2,6 +2,10 @@
2
2
 
3
3
  # Ralph Import - Convert PRDs to Ralph format using Claude Code
4
4
  # Version: 0.9.8 - Modern CLI support with JSON output parsing
5
+ #
6
+ # DEPRECATED: This script is from standalone Ralph and references `ralph-setup`
7
+ # which does not exist in bmalph. Use `bmalph implement` for PRD-to-Ralph
8
+ # transition instead. This file is bundled for backward compatibility only.
5
9
  set -e
6
10
 
7
11
  # Configuration
@@ -281,8 +285,9 @@ HELPEOF
281
285
  # Check dependencies
282
286
  check_dependencies() {
283
287
  if ! command -v ralph-setup &> /dev/null; then
284
- log "ERROR" "Ralph not installed. Run ./install.sh first"
285
- exit 1
288
+ log "WARN" "ralph-setup not found. If using bmalph, run 'bmalph init' instead."
289
+ log "WARN" "This script is deprecated — use 'bmalph implement' for PRD conversion."
290
+ return 1
286
291
  fi
287
292
 
288
293
  if ! command -v jq &> /dev/null; then
@@ -19,7 +19,7 @@ source "$SCRIPT_DIR/lib/circuit_breaker.sh"
19
19
 
20
20
  # Configuration
21
21
  # Ralph-specific files live in .ralph/ subfolder
22
- RALPH_DIR=".ralph"
22
+ RALPH_DIR="${RALPH_DIR:-.ralph}"
23
23
  PROMPT_FILE="$RALPH_DIR/PROMPT.md"
24
24
  LOG_DIR="$RALPH_DIR/logs"
25
25
  DOCS_DIR="$RALPH_DIR/docs/generated"
@@ -239,7 +239,10 @@ setup_tmux_session() {
239
239
  tmux send-keys -t "$session_name:${base_win}.1" "tail -f '$project_dir/$LIVE_LOG_FILE'" Enter
240
240
 
241
241
  # Right-bottom pane (pane 2): Ralph status monitor
242
- if command -v ralph-monitor &> /dev/null; then
242
+ # Prefer bmalph watch (TypeScript, fully tested) over legacy ralph_monitor.sh
243
+ if command -v bmalph &> /dev/null; then
244
+ tmux send-keys -t "$session_name:${base_win}.2" "bmalph watch" Enter
245
+ elif command -v ralph-monitor &> /dev/null; then
243
246
  tmux send-keys -t "$session_name:${base_win}.2" "ralph-monitor" Enter
244
247
  else
245
248
  tmux send-keys -t "$session_name:${base_win}.2" "'$ralph_home/ralph_monitor.sh'" Enter
@@ -404,18 +407,6 @@ can_make_call() {
404
407
  fi
405
408
  }
406
409
 
407
- # Increment call counter
408
- increment_call_counter() {
409
- local calls_made=0
410
- if [[ -f "$CALL_COUNT_FILE" ]]; then
411
- calls_made=$(cat "$CALL_COUNT_FILE")
412
- fi
413
-
414
- ((calls_made++))
415
- echo "$calls_made" > "$CALL_COUNT_FILE"
416
- echo "$calls_made"
417
- }
418
-
419
410
  # Wait for rate limit reset with countdown
420
411
  wait_for_reset() {
421
412
  local calls_made=$(cat "$CALL_COUNT_FILE" 2>/dev/null || echo "0")
@@ -661,10 +652,9 @@ build_loop_context() {
661
652
  echo "${context:0:500}"
662
653
  }
663
654
 
664
- # Get session file age in hours (cross-platform)
665
- # Returns: age in hours on stdout, or -1 if stat fails
666
- # Note: Returns 0 for files less than 1 hour old
667
- get_session_file_age_hours() {
655
+ # Get session file age in seconds (cross-platform)
656
+ # Returns: age in seconds on stdout, or -1 if stat fails
657
+ get_session_file_age_seconds() {
668
658
  local file=$1
669
659
 
670
660
  if [[ ! -f "$file" ]]; then
@@ -700,9 +690,8 @@ get_session_file_age_hours() {
700
690
  current_time=$(date +%s)
701
691
 
702
692
  local age_seconds=$((current_time - file_mtime))
703
- local age_hours=$((age_seconds / 3600))
704
693
 
705
- echo "$age_hours"
694
+ echo "$age_seconds"
706
695
  }
707
696
 
708
697
  # Initialize or resume Claude session (with expiration check)
@@ -723,20 +712,23 @@ get_session_file_age_hours() {
723
712
  init_claude_session() {
724
713
  if [[ -f "$CLAUDE_SESSION_FILE" ]]; then
725
714
  # Check session age
726
- local age_hours
727
- age_hours=$(get_session_file_age_hours "$CLAUDE_SESSION_FILE")
715
+ local age_seconds
716
+ age_seconds=$(get_session_file_age_seconds "$CLAUDE_SESSION_FILE")
728
717
 
729
718
  # Handle stat failure (-1) - treat as needing new session
730
719
  # Don't expire sessions when we can't determine age
731
- if [[ $age_hours -eq -1 ]]; then
720
+ if [[ $age_seconds -eq -1 ]]; then
732
721
  log_status "WARN" "Could not determine session age, starting new session"
733
722
  rm -f "$CLAUDE_SESSION_FILE"
734
723
  echo ""
735
724
  return 0
736
725
  fi
737
726
 
727
+ local expiry_seconds=$((CLAUDE_SESSION_EXPIRY_HOURS * 3600))
728
+
738
729
  # Check if session has expired
739
- if [[ $age_hours -ge $CLAUDE_SESSION_EXPIRY_HOURS ]]; then
730
+ if [[ $age_seconds -ge $expiry_seconds ]]; then
731
+ local age_hours=$((age_seconds / 3600))
740
732
  log_status "INFO" "Session expired (${age_hours}h old, max ${CLAUDE_SESSION_EXPIRY_HOURS}h), starting new session"
741
733
  rm -f "$CLAUDE_SESSION_FILE"
742
734
  echo ""
@@ -746,6 +738,7 @@ init_claude_session() {
746
738
  # Session is valid, try to read it
747
739
  local session_id=$(cat "$CLAUDE_SESSION_FILE" 2>/dev/null)
748
740
  if [[ -n "$session_id" ]]; then
741
+ local age_hours=$((age_seconds / 3600))
749
742
  log_status "INFO" "Resuming Claude session: ${session_id:0:20}... (${age_hours}h old)"
750
743
  echo "$session_id"
751
744
  return 0
@@ -1436,6 +1429,21 @@ main() {
1436
1429
  exit 1
1437
1430
  fi
1438
1431
 
1432
+ # Check required dependencies
1433
+ if ! command -v jq &> /dev/null; then
1434
+ log_status "ERROR" "Required dependency 'jq' is not installed."
1435
+ echo ""
1436
+ echo "jq is required for JSON processing in the Ralph loop."
1437
+ echo ""
1438
+ echo "Install jq:"
1439
+ echo " macOS: brew install jq"
1440
+ echo " Ubuntu: sudo apt-get install jq"
1441
+ echo " Windows: choco install jq (or: winget install jqlang.jq)"
1442
+ echo ""
1443
+ echo "After installing, run this command again."
1444
+ exit 1
1445
+ fi
1446
+
1439
1447
  # Initialize session tracking before entering the loop
1440
1448
  init_session_tracking
1441
1449
 
@@ -1646,6 +1654,9 @@ Examples:
1646
1654
  HELPEOF
1647
1655
  }
1648
1656
 
1657
+ # Only parse arguments and run main when executed directly, not when sourced
1658
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1659
+
1649
1660
  # Parse command line arguments
1650
1661
  while [[ $# -gt 0 ]]; do
1651
1662
  case $1 in
@@ -1753,14 +1764,13 @@ while [[ $# -gt 0 ]]; do
1753
1764
  esac
1754
1765
  done
1755
1766
 
1756
- # Only execute when run directly, not when sourced
1757
- if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1758
- # If tmux mode requested, set it up
1759
- if [[ "$USE_TMUX" == "true" ]]; then
1760
- check_tmux_available
1761
- setup_tmux_session
1762
- fi
1763
-
1764
- # Start the main loop
1765
- main
1767
+ # If tmux mode requested, set it up
1768
+ if [[ "$USE_TMUX" == "true" ]]; then
1769
+ check_tmux_available
1770
+ setup_tmux_session
1766
1771
  fi
1772
+
1773
+ # Start the main loop
1774
+ main
1775
+
1776
+ fi # end: BASH_SOURCE[0] == $0
@@ -1,6 +1,10 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # Ralph Status Monitor - Live terminal dashboard for the Ralph loop
4
+ #
5
+ # DEPRECATED: Use `bmalph watch` instead, which provides a more capable
6
+ # TypeScript-based live dashboard with full test coverage.
7
+ # This script is kept for backward compatibility in tmux sessions.
4
8
  set -e
5
9
 
6
10
  STATUS_FILE=".ralph/status.json"
@@ -0,0 +1,16 @@
1
+ # Check Project Health
2
+
3
+ Run diagnostic checks on the bmalph installation and report any issues.
4
+
5
+ ## How to Run
6
+
7
+ Execute the CLI command:
8
+ bmalph doctor
9
+
10
+ ## What It Does
11
+
12
+ - Verifies required directories exist (`_bmad/`, `.ralph/`, `bmalph/`)
13
+ - Checks that slash commands are installed correctly
14
+ - Validates the instructions file contains the BMAD snippet
15
+ - Reports version mismatches between installed and bundled assets
16
+ - Suggests remediation steps for any issues found