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.
- package/README.md +105 -38
- package/dist/cli.js +19 -0
- package/dist/commands/doctor.d.ts +0 -11
- package/dist/commands/doctor.js +22 -55
- package/dist/commands/implement.d.ts +6 -0
- package/dist/commands/implement.js +82 -0
- package/dist/commands/init.js +4 -2
- package/dist/commands/reset.d.ts +7 -0
- package/dist/commands/reset.js +81 -0
- package/dist/commands/status.js +100 -11
- package/dist/commands/watch.d.ts +6 -0
- package/dist/commands/watch.js +19 -0
- package/dist/installer.d.ts +0 -6
- package/dist/installer.js +0 -10
- package/dist/platform/claude-code.js +0 -1
- package/dist/reset.d.ts +18 -0
- package/dist/reset.js +181 -0
- package/dist/transition/artifact-scan.d.ts +27 -0
- package/dist/transition/artifact-scan.js +91 -0
- package/dist/transition/artifacts.d.ts +0 -1
- package/dist/transition/artifacts.js +0 -26
- package/dist/transition/context.js +34 -0
- package/dist/transition/fix-plan.d.ts +8 -2
- package/dist/transition/fix-plan.js +33 -7
- package/dist/transition/index.d.ts +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/transition/orchestration.d.ts +2 -2
- package/dist/transition/orchestration.js +120 -41
- package/dist/transition/preflight.d.ts +6 -0
- package/dist/transition/preflight.js +154 -0
- package/dist/transition/specs-index.d.ts +1 -1
- package/dist/transition/specs-index.js +24 -1
- package/dist/transition/types.d.ts +23 -1
- package/dist/utils/dryrun.d.ts +1 -1
- package/dist/utils/dryrun.js +22 -0
- package/dist/utils/state.d.ts +0 -2
- package/dist/utils/validate.js +3 -2
- package/dist/watch/dashboard.d.ts +4 -0
- package/dist/watch/dashboard.js +60 -0
- package/dist/watch/file-watcher.d.ts +9 -0
- package/dist/watch/file-watcher.js +27 -0
- package/dist/watch/renderer.d.ts +16 -0
- package/dist/watch/renderer.js +241 -0
- package/dist/watch/state-reader.d.ts +9 -0
- package/dist/watch/state-reader.js +190 -0
- package/dist/watch/types.d.ts +55 -0
- package/dist/watch/types.js +1 -0
- package/package.json +9 -4
- package/ralph/lib/circuit_breaker.sh +86 -59
- package/ralph/lib/enable_core.sh +3 -6
- package/ralph/lib/response_analyzer.sh +5 -29
- package/ralph/lib/task_sources.sh +45 -11
- package/ralph/lib/wizard_utils.sh +9 -0
- package/ralph/ralph_import.sh +7 -2
- package/ralph/ralph_loop.sh +44 -34
- package/ralph/ralph_monitor.sh +4 -0
- package/slash-commands/bmalph-doctor.md +16 -0
- package/slash-commands/bmalph-implement.md +18 -141
- package/slash-commands/bmalph-status.md +15 -0
- package/slash-commands/bmalph-upgrade.md +15 -0
- 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.
|
|
4
|
-
"description": "Unified AI Development Framework - BMAD phases with Ralph execution loop
|
|
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:
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
}
|
package/ralph/lib/enable_core.sh
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
832
|
+
local now
|
|
833
|
+
now=$(get_epoch_seconds)
|
|
833
834
|
local session_time
|
|
835
|
+
session_time=$(parse_iso_to_epoch "$timestamp")
|
|
834
836
|
|
|
835
|
-
#
|
|
836
|
-
#
|
|
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
|
|
313
|
-
|
|
314
|
-
if [[ -n "$
|
|
315
|
-
# Extract
|
|
316
|
-
while IFS= read -r
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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 #
|
|
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))
|
package/ralph/ralph_import.sh
CHANGED
|
@@ -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 "
|
|
285
|
-
|
|
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
|
package/ralph/ralph_loop.sh
CHANGED
|
@@ -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="
|
|
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
|
-
|
|
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
|
|
665
|
-
# Returns: age in
|
|
666
|
-
|
|
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 "$
|
|
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
|
|
727
|
-
|
|
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 [[ $
|
|
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 [[ $
|
|
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
|
-
#
|
|
1757
|
-
if [[ "$
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
package/ralph/ralph_monitor.sh
CHANGED
|
@@ -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
|