spec-and-loop 1.0.0 → 1.0.2

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/QUICKSTART.md CHANGED
@@ -8,10 +8,10 @@ Install these tools (one-time setup):
8
8
 
9
9
  ```bash
10
10
  # 1. Install openspec (OpenSpec CLI)
11
- npm install -g openspec
11
+ npm install -g @fission-ai/openspec@latest
12
12
 
13
13
  # 2. Install opencode (agentic coding assistant)
14
- npm install -g opencode
14
+ npm install -g opencode-ai
15
15
 
16
16
  # 3. Install jq (command-line JSON processor)
17
17
  # Ubuntu/Debian:
@@ -32,7 +32,7 @@ npm install -g spec-and-loop
32
32
 
33
33
  **Prerequisites:** Install openspec and opencode:
34
34
  ```bash
35
- npm install -g openspec opencode
35
+ npm install -g @fission-ai/openspec@latest opencode-ai
36
36
  ```
37
37
 
38
38
  ## Quick Demo (5 Minutes)
@@ -47,10 +47,10 @@ git init
47
47
  openspec init
48
48
 
49
49
  # 3. Create a new change
50
- opsx-new add-hello-world
50
+ openspec new add-hello-world
51
51
 
52
52
  # 4. Fast-forward through artifact creation
53
- opsx-ff
53
+ openspec ff
54
54
 
55
55
  # 5. Run the ralph loop (executes tasks with opencode)
56
56
  ralph-run --change add-hello-world
@@ -156,7 +156,7 @@ git diff HEAD~15 # See full implementation
156
156
  ### "openspec CLI not found" or "opencode CLI not found"
157
157
 
158
158
  ```bash
159
- npm install -g openspec opencode
159
+ npm install -g @fission-ai/openspec@latest opencode-ai
160
160
  ```
161
161
 
162
162
  ### "jq CLI not found"
@@ -198,7 +198,7 @@ source ~/.bashrc
198
198
  grep "^\- \[x\]" openspec/changes/my-feature/tasks.md
199
199
 
200
200
  # Or create a new change
201
- opsx-new another-feature
201
+ openspec new another-feature
202
202
  ```
203
203
 
204
204
  ## Features at a Glance
package/README.md CHANGED
@@ -8,41 +8,35 @@ OpenSpec + Ralph Loop integration for iterative development with opencode.
8
8
 
9
9
  OpenSpec provides excellent structure for planning (proposal → specs → design → tasks) but leaves execution manual. Ralph Wiggum's iterative development loop (execute → commit → repeat) is powerful but requires PRD format instead of OpenSpec specs.
10
10
 
11
- **This utility bridges the gap**: use OpenSpec for planning, then automatically execute tasks with full context using opencode agentic coding assistant.
12
-
13
11
  ## Installation
14
12
 
15
13
  ```bash
16
14
  npm install -g spec-and-loop
17
15
  ```
18
16
 
19
- **Prerequisites:** You need openspec and opencode installed:
17
+ **Prerequisites:** You need OpenSpec and the OpenCode AI agent installed:
20
18
 
21
19
  ```bash
22
- npm install -g openspec opencode
20
+ # Install OpenSpec and OpenCode (recommended)
21
+ npm install -g @fission-ai/openspec@latest opencode-ai
23
22
  ```
24
23
 
25
- ## Quick Start
26
-
27
- **[🚀 Get Started in 5 Minutes](./QUICKSTART.md)**
24
+ Alternative OpenCode install methods (if you prefer):
28
25
 
29
26
  ```bash
30
- # 1. Initialize OpenSpec in your project
31
- openspec init
27
+ # npm (recommended)
28
+ npm install -g opencode-ai
32
29
 
33
- # 2. Create a new change
34
- openspec new add-user-auth
30
+ # Install script (general use)
31
+ curl -fsSL https://opencode.ai/install | bash
35
32
 
36
- # 3. Fast-forward through artifact creation
37
- openspec ff add-user-auth
33
+ # Homebrew (macOS / Linux)
34
+ brew install anomalyco/tap/opencode
38
35
 
39
- # 4. Run the ralph loop (executes tasks with opencode)
40
- ralph-run --change add-user-auth
36
+ # Windows: use WSL and install via one of the Linux methods above
41
37
  ```
42
38
 
43
- For detailed step-by-step instructions, see [QUICKSTART.md](./QUICKSTART.md).
44
-
45
- ## Quick Start
39
+ **[🚀 Get Started in 5 Minutes](./QUICKSTART.md)**
46
40
 
47
41
  ```bash
48
42
  # 1. Initialize OpenSpec in your project
@@ -58,30 +52,28 @@ openspec ff add-user-auth
58
52
  ralph-run --change add-user-auth
59
53
  ```
60
54
 
61
- Or auto-detect the most recent change:
55
+ For detailed step-by-step instructions, see [QUICKSTART.md](./QUICKSTART.md).
62
56
 
63
- ```bash
64
- ralph-run
65
- ```
57
+ <!-- Duplicate Quick Start removed; see QUICKSTART.md for full instructions -->
66
58
 
67
59
  ## Prerequisites
68
60
 
69
61
  Before using spec-and-loop, ensure you have:
70
62
 
71
- 1. **Node.js & npm** - For package installation
63
+ 1. **Node.js** - For package installation
72
64
  ```bash
73
- node --version # Should be 14+
74
- npm --version # Should be 6+
65
+ node --version # Should be >=24
75
66
  ```
76
67
 
77
68
  2. **openspec** - OpenSpec CLI for specification workflow
78
69
  ```bash
79
- npm install -g openspec
70
+ npm install -g @fission-ai/openspec@latest
80
71
  ```
81
72
 
82
73
  3. **opencode** - Agentic coding assistant
83
74
  ```bash
84
- npm install -g opencode
75
+ # Install via npm
76
+ npm install -g opencode-ai
85
77
  ```
86
78
 
87
79
  4. **jq** - Command-line JSON processor
@@ -321,7 +313,7 @@ For common issues and solutions, see [QUICKSTART.md#troubleshooting](./QUICKSTAR
321
313
 
322
314
  ```bash
323
315
  # opencode not found?
324
- npm install -g opencode
316
+ npm install -g opencode-ai
325
317
 
326
318
  # jq not found?
327
319
  sudo apt install jq # or: brew install jq
@@ -341,4 +333,4 @@ export PATH="$PATH:$(npm root -g)/.bin"
341
333
 
342
334
  ## License
343
335
 
344
- MIT
336
+ GPL-3.0
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "spec-and-loop",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
5
5
  "main": "index.js",
6
6
  "bin": {
7
- "ralph-run": "./bin/ralph-run"
7
+ "ralph-run": "bin/ralph-run"
8
8
  },
9
9
  "scripts": {
10
10
  "postinstall": "node scripts/setup.js"
11
11
  },
12
12
  "dependencies": {
13
- "openspec": "^1.0.0"
13
+ "@fission-ai/openspec": "latest"
14
14
  },
15
15
  "keywords": [
16
16
  "openspec",
@@ -23,7 +23,7 @@
23
23
  "license": "GPL-3.0",
24
24
  "repository": {
25
25
  "type": "git",
26
- "url": "https://github.com/ncheaz/spec-and-loop.git"
26
+ "url": "git+https://github.com/ncheaz/spec-and-loop.git"
27
27
  },
28
28
  "files": [
29
29
  "bin/",
@@ -0,0 +1,96 @@
1
+ #!/bin/bash
2
+
3
+ set -e
4
+
5
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ CHANGE_NAME="${1:-auto-detect}"
7
+
8
+ if [[ "$CHANGE_NAME" == "auto-detect" ]]; then
9
+ # Auto-detect most recent change
10
+ CHANGE_NAME=$(ls -t openspec/changes/ 2>/dev/null | head -1)
11
+ if [[ -z "$CHANGE_NAME" ]]; then
12
+ echo "No changes found in openspec/changes/"
13
+ exit 1
14
+ fi
15
+ fi
16
+
17
+ TASKS_FILE="$SCRIPT_DIR/../openspec/changes/$CHANGE_NAME/tasks.md"
18
+ RALPH_STATE="$SCRIPT_DIR/../.ralph/ralph-loop.state.json"
19
+
20
+ if [[ ! -f "$TASKS_FILE" ]]; then
21
+ echo "Tasks file not found: $TASKS_FILE"
22
+ exit 1
23
+ fi
24
+
25
+ clear
26
+ while true; do
27
+ clear
28
+
29
+ echo "======================================================================="
30
+ echo " Ralph Progress Monitor - Change: $CHANGE_NAME"
31
+ echo "======================================================================="
32
+ echo ""
33
+
34
+ # Progress bar
35
+ total=$(grep -c "^- \[" "$TASKS_FILE" 2>/dev/null) || total=0
36
+ completed=$(grep -c "^- \[x\]" "$TASKS_FILE" 2>/dev/null) || completed=0
37
+ in_progress=$(grep -c "^- \[\/\]" "$TASKS_FILE" 2>/dev/null) || in_progress=0
38
+
39
+ remaining=$((total - completed - in_progress))
40
+
41
+ if [[ $total -gt 0 ]]; then
42
+ progress=$((completed * 100 / total))
43
+ echo " Progress: $completed/$total tasks completed ($progress%)"
44
+ echo " Status: $in_progress in progress, $remaining remaining"
45
+
46
+ # Simple progress bar
47
+ filled=$((completed * 50 / total))
48
+ bar=$(printf '#%.0s' $(seq 1 $filled))
49
+ empty=$(printf '.%.0s' $(seq 1 $((50 - filled))))
50
+ echo " |$bar$empty|"
51
+ fi
52
+ echo ""
53
+
54
+ # Ralph state
55
+ if [[ -f "$RALPH_STATE" ]]; then
56
+ echo " Ralph State:"
57
+ jq -r '
58
+ " Iteration: \(.iteration)/\(.maxIterations)",
59
+ " Active: \(.active)",
60
+ " Mode: \(.tasksMode // "N/A")",
61
+ " Prompt: \(.completionPromise // "N/A")"
62
+ ' "$RALPH_STATE" 2>/dev/null || echo " Unable to read state"
63
+ echo ""
64
+ fi
65
+
66
+ # Current task
67
+ echo " Current Task:"
68
+ current_task=$(grep -n "^- \[\/\]" "$TASKS_FILE" 2>/dev/null | head -1)
69
+ if [[ -n "$current_task" ]]; then
70
+ echo " $current_task"
71
+ else
72
+ # Show next incomplete task
73
+ next_task=$(grep -n "^- \[ \]" "$TASKS_FILE" 2>/dev/null | head -1)
74
+ if [[ -n "$next_task" ]]; then
75
+ echo " Next: $next_task"
76
+ else
77
+ echo " No current task (idle)"
78
+ fi
79
+ fi
80
+ echo ""
81
+
82
+ # Recent completed tasks (last 3)
83
+ echo " Recently Completed:"
84
+ grep "^- \[x\]" "$TASKS_FILE" 2>/dev/null | tail -3 | sed 's/^- \[x\] / /'
85
+ echo ""
86
+
87
+ # File status
88
+ echo " Last Updated: $(stat -c "%y" "$TASKS_FILE" 2>/dev/null | cut -d'.' -f1)"
89
+ echo ""
90
+
91
+ echo "======================================================================="
92
+ echo " Press Ctrl+C to exit | Refreshing in 2 seconds..."
93
+ echo "======================================================================="
94
+
95
+ sleep 2
96
+ done
@@ -1,11 +1,44 @@
1
1
  #!/bin/bash
2
2
 
3
3
  set -e
4
- trap 'handle_error $?' EXIT
5
4
 
6
5
  VERSION="1.0.0"
6
+
7
+ # Add Bun to PATH if installed
8
+ if [[ -d "$HOME/.bun/bin" ]]; then
9
+ export PATH="$HOME/.bun/bin:$PATH"
10
+ fi
11
+
7
12
  CHANGE_NAME=""
13
+ MAX_ITERATIONS=""
8
14
  ERROR_OCCURRED=false
15
+ CLEANUP_IN_PROGRESS=false
16
+
17
+ # Trap signals for proper cleanup
18
+ cleanup() {
19
+ # Prevent multiple cleanup calls
20
+ if [[ "$CLEANUP_IN_PROGRESS" == "true" ]]; then
21
+ return 0
22
+ fi
23
+ CLEANUP_IN_PROGRESS=true
24
+
25
+ local exit_code=$1
26
+ log_info "Cleaning up..."
27
+
28
+ # NOTE: We do NOT kill ralph/bun processes here because:
29
+ # 1. Ralph runs synchronously in the foreground
30
+ # 2. Ctrl+C (SIGINT) naturally propagates to child processes
31
+ # 3. Using pkill -f "bun" is DANGEROUS - it matches gnome-session-binary!
32
+ # 4. Using pkill -f "ralph" could kill other user processes
33
+ # The shell's process group handling ensures clean termination.
34
+
35
+ if [[ $exit_code -ne 0 ]]; then
36
+ log_error "Script terminated with exit code: $exit_code"
37
+ fi
38
+ exit $exit_code
39
+ }
40
+
41
+ trap 'cleanup $?' EXIT INT TERM QUIT
9
42
 
10
43
  handle_error() {
11
44
  local exit_code=$1
@@ -26,20 +59,22 @@ USAGE:
26
59
  ralph-run [OPTIONS]
27
60
 
28
61
  OPTIONS:
29
- --change <name> Specify the OpenSpec change to execute (default: auto-detect)
30
- --verbose, -v Enable verbose mode for debugging
31
- --help, -h Show this help message
62
+ --change <name> Specify the OpenSpec change to execute (default: auto-detect)
63
+ --max-iterations <n> Maximum iterations for Ralph loop (default: 50)
64
+ --verbose, -v Enable verbose mode for debugging
65
+ --help, -h Show this help message
32
66
 
33
67
  EXAMPLES:
34
68
  ralph-run # Auto-detect most recent change
35
69
  ralph-run --change my-feature # Execute specific change
70
+ ralph-run --max-iterations 100 # Limit loop to 100 iterations
36
71
  ralph-run --verbose # Run with debug output
37
72
 
38
73
  PREREQUISITES:
39
74
  - Git repository (git init)
40
75
  - OpenSpec artifacts created (openspec init, opsx-new, opsx-ff)
76
+ - ralph CLI installed (npm install -g @th0rgal/ralph-wiggum)
41
77
  - opencode CLI installed (npm install -g opencode)
42
- - jq CLI installed (apt install jq / brew install jq)
43
78
 
44
79
  EOF
45
80
  }
@@ -51,6 +86,10 @@ parse_arguments() {
51
86
  CHANGE_NAME="$2"
52
87
  shift 2
53
88
  ;;
89
+ --max-iterations)
90
+ MAX_ITERATIONS="$2"
91
+ shift 2
92
+ ;;
54
93
  --verbose|-v)
55
94
  VERBOSE=true
56
95
  shift
@@ -75,12 +114,12 @@ parse_arguments() {
75
114
 
76
115
  log_verbose() {
77
116
  if [[ "$VERBOSE" == true ]]; then
78
- echo "[VERBOSE] $*"
117
+ echo "[VERBOSE] $*" >&2
79
118
  fi
80
119
  }
81
120
 
82
121
  log_info() {
83
- echo "[INFO] $*"
122
+ echo "[INFO] $*" >&2
84
123
  }
85
124
 
86
125
  log_error() {
@@ -101,7 +140,15 @@ validate_git_repository() {
101
140
 
102
141
  validate_dependencies() {
103
142
  log_verbose "Validating dependencies..."
104
-
143
+
144
+ # Check for ralph
145
+ if ! command -v ralph &> /dev/null; then
146
+ log_error "ralph CLI not found."
147
+ log_error "Please install open-ralph-wiggum: npm install -g @th0rgal/ralph-wiggum"
148
+ exit 1
149
+ fi
150
+ log_verbose "Found: ralph"
151
+
105
152
  # Check for opencode
106
153
  if ! command -v opencode &> /dev/null; then
107
154
  log_error "opencode CLI not found."
@@ -109,17 +156,7 @@ validate_dependencies() {
109
156
  exit 1
110
157
  fi
111
158
  log_verbose "Found: opencode"
112
-
113
- # Check for jq
114
- if ! command -v jq &> /dev/null; then
115
- log_error "jq CLI not found."
116
- log_error "Please install jq:"
117
- log_error " Ubuntu/Debian: apt install jq"
118
- log_error " macOS: brew install jq"
119
- exit 1
120
- fi
121
- log_verbose "Found: jq"
122
-
159
+
123
160
  log_verbose "All dependencies validated"
124
161
  }
125
162
 
@@ -250,7 +287,16 @@ generate_prd() {
250
287
  prd_content+="$OPENSPEC_SPECS"$'\n'$'\n'
251
288
 
252
289
  prd_content+="## Design"$'\n'$'\n'
253
- prd_content+="$OPENSPEC_DESIGN"$'\n'
290
+ prd_content+="$OPENSPEC_DESIGN"$'\n'$'\n'
291
+
292
+ # Add current task context for Ralph to use in commits
293
+ local task_context
294
+ task_context=$(get_current_task_context "$change_dir")
295
+
296
+ if [[ -n "$task_context" ]]; then
297
+ prd_content+="## Current Task Context"$'\n'$'\n'
298
+ prd_content+="$task_context"$'\n'$'\n'
299
+ fi
254
300
 
255
301
  echo "$prd_content"
256
302
  }
@@ -473,22 +519,13 @@ get_git_history() {
473
519
  gather_opencode_context() {
474
520
  local task_description="$1"
475
521
 
476
- log_verbose "Gathering opencode context..."
522
+ log_verbose "Gathering minimal opencode context..."
477
523
 
478
524
  local context=""
479
525
 
480
526
  context+="## Task"$'\n'
481
527
  context+="$task_description"$'\n'$'\n'
482
528
 
483
- context+="## Proposal Summary"$'\n'
484
- context+="$(echo "$OPENSPEC_PROPOSAL" | head -20)"$'\n'$'\n'
485
-
486
- context+="## Design Decisions"$'\n'
487
- context+="$(echo "$OPENSPEC_DESIGN" | head -30)"$'\n'$'\n'
488
-
489
- context+="## Specifications"$'\n'
490
- context+="$(echo "$OPENSPEC_SPECS" | head -50)"$'\n'$'\n'
491
-
492
529
  echo "$context"
493
530
  }
494
531
 
@@ -496,227 +533,357 @@ generate_opencode_prompt() {
496
533
  local task_description="$1"
497
534
  local ralph_dir="$2"
498
535
 
499
- log_verbose "Generating opencode prompt..."
536
+ log_verbose "Generating minimal opencode prompt..."
500
537
 
501
538
  local context
502
539
  context=$(gather_opencode_context "$task_description")
503
540
 
504
541
  local prompt=""
505
- prompt+="You are implementing a task as part of an OpenSpec change."$'\n'$'\n'
542
+ prompt+="Implement this task:"$'\n'
506
543
  prompt+="$context"$'\n'
507
544
 
508
- prompt+="## Recent Git History"$'\n'
509
- local git_history
510
- git_history=$(get_git_history 10)
511
- if [[ -n "$git_history" ]]; then
512
- echo "$git_history" | while IFS='|' read -r hash date author message; do
513
- prompt+="- $hash ($date $author): $message"$'\n'
514
- done
515
- fi
516
- prompt+=$'\n'
517
-
518
- prompt+="## Error History"$'\n'
519
- local errors_file="$ralph_dir/errors.md"
520
- if [[ -f "$errors_file" ]]; then
521
- prompt+="$(cat "$errors_file" | tail -50)"$'\n'
522
- else
523
- prompt+="(No previous errors)"$'\n'
524
- fi
525
- prompt+=$'\n'
526
-
527
- prompt+="## Instructions"$'\n'
528
- prompt+="Implement the task above. Use the context provided to understand the requirements and any relevant design decisions."$'\n'
529
- prompt+="If there are previous errors, use them to guide your implementation to avoid repeating mistakes."$'\n'
530
-
531
- local injected_context
532
- if injected_context=$(handle_context_injection "$ralph_dir"); then
533
- prompt+=$'\n'
534
- prompt+="## Injected Context"$'\n'
535
- prompt+="$injected_context"$'\n'
536
- fi
537
-
538
545
  echo "$prompt"
539
546
  }
540
547
 
541
- execute_opencode() {
542
- local prompt="$1"
548
+ sync_tasks_to_ralph() {
549
+ local change_dir="$1"
550
+ local ralph_dir="$2"
543
551
 
544
- log_verbose "Executing opencode CLI..."
552
+ local tasks_file="$change_dir/tasks.md"
553
+ local ralph_tasks_file=".ralph/ralph-tasks.md"
554
+ local old_ralph_tasks_file="$change_dir/.ralph/ralph-tasks.md"
545
555
 
546
- if ! command -v opencode &> /dev/null; then
547
- log_error "opencode CLI not found. Please install opencode."
556
+ if [[ ! -f "$tasks_file" ]]; then
557
+ log_error "Tasks file not found: $tasks_file"
548
558
  return 1
549
559
  fi
550
560
 
551
- local output
552
- output=$(echo "$prompt" | opencode 2>&1)
553
- local exit_code=$?
561
+ local abs_tasks_file=$(realpath "$tasks_file" 2>/dev/null)
554
562
 
555
- echo "$output"
556
- return $exit_code
557
- }
558
-
559
- create_git_commit() {
560
- local task_description="$1"
561
-
562
- log_verbose "Creating git commit..."
563
+ # Clean up old Ralph tasks file in change directory if exists
564
+ if [[ -f "$old_ralph_tasks_file" ]]; then
565
+ log_verbose "Removing old Ralph tasks file from change directory: $old_ralph_tasks_file"
566
+ rm "$old_ralph_tasks_file"
567
+ fi
563
568
 
564
- if ! git diff-index --quiet HEAD --; then
565
- git add -A
566
- git commit -m "$task_description" 2>&1
567
- log_verbose "Git commit created"
569
+ # Use symlink so Ralph and openspec-apply-change work on the SAME file
570
+ if [[ -L "$ralph_tasks_file" ]]; then
571
+ log_verbose "Symlink exists, ensuring it points to correct location"
572
+ local current_target=$(readlink -f "$ralph_tasks_file" 2>/dev/null || echo "")
573
+
574
+ if [[ "$current_target" != "$abs_tasks_file" ]]; then
575
+ log_verbose "Updating symlink to point to new change directory"
576
+ ln -sf "$abs_tasks_file" "$ralph_tasks_file"
577
+ fi
578
+ elif [[ -f "$ralph_tasks_file" ]]; then
579
+ # File exists but is not a symlink - replace with symlink
580
+ log_verbose "Replacing regular file with symlink to openspec tasks..."
581
+ rm "$ralph_tasks_file"
582
+ ln -sf "$abs_tasks_file" "$ralph_tasks_file"
568
583
  else
569
- log_verbose "No changes to commit"
584
+ # No file exists - create new symlink
585
+ log_verbose "Creating symlink from .ralph/ralph-tasks.md to openspec tasks..."
586
+ ln -sf "$abs_tasks_file" "$ralph_tasks_file"
570
587
  fi
588
+
589
+ log_verbose "Symlink configured: $ralph_tasks_file -> $abs_tasks_file"
571
590
  }
572
591
 
573
- execute_task_loop() {
574
- local ralph_dir="$1"
592
+ create_prompt_template() {
593
+ local change_dir="$1"
594
+ local template_file="$2"
575
595
 
576
- log_info "Starting task execution loop..."
596
+ log_verbose "Creating custom prompt template..."
577
597
 
578
- local total_tasks=${#TASKS[@]}
579
- log_info "Total tasks to execute: $total_tasks"
598
+ local abs_change_dir
599
+ abs_change_dir=$(realpath "$change_dir" 2>/dev/null)
580
600
 
581
- for i in "${!TASKS[@]}"; do
582
- local task_description="${TASKS[$i]}"
583
- local task_id="${TASK_IDS[$i]}"
584
-
585
- log_info "Executing task $((i+1))/$total_tasks: $task_description"
586
-
587
- local prompt
588
- prompt=$(generate_opencode_prompt "$task_description" "$ralph_dir")
589
-
590
- local output
591
- output=$(execute_opencode "$prompt")
592
- local exit_code=$?
593
-
594
- if [[ $exit_code -eq 0 ]]; then
595
- log_info "Task completed successfully"
596
- create_git_commit "$task_description"
597
- else
598
- log_error "Task failed with exit code: $exit_code"
599
- log_error "Output: $output"
600
- break
601
- fi
602
- done
601
+ cat > "$template_file" << 'EOF'
602
+ # Ralph Wiggum Task Execution - Iteration {{iteration}} / {{max_iterations}}
603
+
604
+ Change directory: {{change_dir}}
605
+
606
+ ## OpenSpec Artifacts Context
607
+
608
+ Include full context from openspec artifacts in {{change_dir}}:
609
+ - Read {{change_dir}}/proposal.md for the overall project goal
610
+ - Read {{change_dir}}/design.md for the technical design approach
611
+ - Read {{change_dir}}/specs/*/spec.md for the detailed specifications
612
+
613
+ ## Task List
614
+
615
+ {{tasks}}
616
+
617
+ ## Instructions
618
+
619
+ 1. **Identify** current task:
620
+ - Find any task marked as [/] (in progress)
621
+ - If no task is in progress, pick the first task marked as [ ] (incomplete)
622
+ - Mark the task as [/] in the tasks file before starting work
623
+
624
+ 2. **Implement** task using openspec-apply-change:
625
+ - Use the /opsx-apply skill to implement the current task
626
+ - Read the relevant openspec artifacts for context (proposal.md, design.md, specs)
627
+ - Follow the openspec workflow to complete the task
628
+ - The openspec-apply-change skill will implement changes and update task status automatically
629
+
630
+ 3. **Complete** task:
631
+ - Verify that the implementation meets the requirements
632
+ - When the task is successfully completed, mark it as [x] in the tasks file
633
+ - Output: `<promise>{{task_promise}}</promise>`
634
+
635
+ 4. **Continue** to the next task:
636
+ - The loop will continue with the next iteration
637
+ - Find the next incomplete task and repeat the process
638
+
639
+ ## Critical Rules
640
+
641
+ - Work on ONE task at a time from the task list
642
+ - Use openspec-apply-change (/opsx-apply) for implementation
643
+ - ONLY output `<promise>{{task_promise}}</promise>` when the current task is complete and marked as [x]
644
+ - ONLY output `<promise>{{completion_promise}}</promise>` when ALL tasks are [x]
645
+ - Output promise tags DIRECTLY - do not quote them, explain them, or say you "will" output them
646
+ - Do NOT lie or output false promises to exit the loop
647
+ - If stuck, try a different approach
648
+ - Check your work before claiming completion
649
+
650
+ ## CRITICAL: Git Commit Format (MANDATORY)
651
+
652
+ When making git commits, you MUST use this EXACT format:
653
+
654
+ ```
655
+ Ralph iteration <N>: <brief description of work completed>
656
+
657
+ Tasks completed:
658
+ - [x] <task.number> <task description text>
659
+ - [x] <task.number> <task description text>
660
+ ...
661
+ ```
662
+
663
+ **Requirements:**
664
+ 1. Use iteration number from Ralph's state (e.g., "Ralph iteration 7")
665
+ 2. Include a BRIEF description summarizing what was done
666
+ 3. List ALL completed tasks with their numbers and full descriptions
667
+ 4. Use the EXACT format: "- [x] <task.number> <task description>"
668
+ 5. Read the "## Completed Tasks for Git Commit" section from the PRD for the task list
669
+
670
+ **FORBIDDEN:**
671
+ - DO NOT use generic messages like "work in progress" or "iteration N"
672
+ - DO NOT skip task numbers
673
+ - DO NOT truncate task descriptions
674
+ - DO NOT create commits without task information
675
+
676
+ **Example:**
677
+ ```
678
+ Ralph iteration 7: Implement unit tests for response processing
679
+
680
+ Tasks completed:
681
+ - [x] 11.6 Write unit test for personality state management
682
+ - [x] 11.7 Write unit test for personality validation
683
+ - [x] 11.8 Write unit test for system prompt validation
684
+ ```
685
+
686
+ {{context}}
687
+ EOF
603
688
 
604
- log_info "Task execution loop complete"
689
+ sed -i "s|{{change_dir}}|$abs_change_dir|g" "$template_file"
690
+
691
+ log_verbose "Prompt template created: $template_file"
605
692
  }
606
693
 
607
- initialize_tracking() {
608
- local ralph_dir="$1"
609
- local tracking_file="$ralph_dir/tracking.json"
694
+ restore_ralph_state_from_tasks() {
695
+ local tasks_file="$1"
696
+ local ralph_loop_file=".ralph/ralph-loop.state.json"
610
697
 
611
- log_verbose "Initializing tracking..."
612
-
613
- if [[ ! -f "$tracking_file" ]]; then
614
- log_verbose "Creating tracking.json..."
615
- echo '{"tasks":{}}' > "$tracking_file"
698
+ if [[ ! -f "$ralph_loop_file" ]]; then
699
+ log_verbose "No Ralph state file found, nothing to restore"
700
+ return 0
616
701
  fi
617
702
 
618
- echo "$tracking_file"
619
- }
620
-
621
- read_tracking() {
622
- local tracking_file="$1"
703
+ # Read current iteration from state file - don't use completed task count
704
+ local current_iteration
705
+ current_iteration=$(jq -r '.iteration // 0' "$ralph_loop_file" 2>/dev/null || echo "0")
623
706
 
624
- log_verbose "Reading tracking.json..."
707
+ # Count completed tasks for informational purposes only
708
+ local completed_count=$(grep -c "^- \[x\]" "$tasks_file" 2>/dev/null || echo "0")
709
+ log_verbose "Found $completed_count completed tasks in tasks.md (iteration $current_iteration)"
625
710
 
626
- if [[ -f "$tracking_file" ]]; then
627
- cat "$tracking_file"
711
+ # Read maxIterations from state file
712
+ local max_iterations
713
+ max_iterations=$(jq -r '.maxIterations // 50' "$ralph_loop_file" 2>/dev/null || echo "50")
714
+
715
+ # Only update iteration if it's 0 or missing (fresh start)
716
+ if [[ $current_iteration -eq 0 ]]; then
717
+ log_verbose "Setting initial iteration to 1 (max: $max_iterations)"
718
+ local updated_state
719
+ updated_state=$(jq --argjson state "$(cat "$ralph_loop_file")" '
720
+ .iteration = 1 |
721
+ .active = true
722
+ ' 2>/dev/null)
723
+
724
+ if [[ -n "$updated_state" ]]; then
725
+ echo "$updated_state" > "$ralph_loop_file"
726
+ fi
628
727
  else
629
- echo '{"tasks":{}}'
728
+ log_verbose "Ralph state preserved at iteration $current_iteration"
630
729
  fi
631
730
  }
632
731
 
633
- update_task_checkbox() {
732
+ get_current_task_context() {
634
733
  local change_dir="$1"
635
- local task_id="$2"
636
- local complete="$3"
734
+ local tasks_file="$change_dir/tasks.md"
637
735
 
638
- log_verbose "Updating task checkbox (line $task_id, complete=$complete)..."
736
+ if [[ ! -f "$tasks_file" ]]; then
737
+ echo ""
738
+ return
739
+ fi
639
740
 
640
- local tasks_file="$change_dir/tasks.md"
641
- local temp_file=$(mktemp)
741
+ log_verbose "Reading current task context from tasks.md..."
642
742
 
743
+ local context=""
744
+ local found_task=false
745
+ local all_completed_tasks=""
746
+ local current_task_desc=""
747
+
748
+ while IFS= read -r line; do
749
+ if [[ "$line" =~ ^-\ \[/\] ]]; then
750
+ # Found in-progress task - extract description
751
+ current_task_desc="${line#- [/] }"
752
+ found_task=true
753
+ break
754
+ elif [[ "$line" =~ ^-\ \[\ \] ]] && [[ "$found_task" == "false" ]]; then
755
+ # Found incomplete task - extract description
756
+ current_task_desc="${line#- [ ] }"
757
+ found_task=true
758
+ break
759
+ fi
760
+ done < "$tasks_file"
761
+
762
+ # Also collect all completed tasks for commit message
643
763
  local line_number=0
644
764
  while IFS= read -r line; do
645
765
  ((line_number++)) || true
646
-
647
- if [[ $line_number -eq $task_id ]]; then
648
- if [[ "$complete" == "true" ]]; then
649
- echo "${line/- \[ \]/- [x]}" >> "$temp_file"
650
- else
651
- echo "${line/- \[x\]/- [ ]}" >> "$temp_file"
652
- fi
653
- else
654
- echo "$line" >> "$temp_file"
766
+ if [[ "$line" =~ ^-\ \[x\] ]]; then
767
+ # Use the full line as-is to preserve task number and description
768
+ all_completed_tasks+="$line"$'\n'
655
769
  fi
656
770
  done < "$tasks_file"
657
771
 
658
- mv "$temp_file" "$tasks_file"
772
+ if [[ "$found_task" == "true" ]]; then
773
+ context="## Current Task"$'\n'
774
+ context+="- $current_task_desc"$'\n'
775
+ fi
776
+
777
+ if [[ -n "$all_completed_tasks" ]]; then
778
+ context+="## Completed Tasks for Git Commit"$'\n'
779
+ context+="$all_completed_tasks"
780
+ fi
781
+
782
+ echo "$context"
659
783
  }
660
784
 
661
- update_tracking() {
662
- local tracking_file="$1"
663
- local task_id="$2"
664
- local complete="$3"
785
+ setup_output_capture() {
786
+ local ralph_dir="$1"
665
787
 
666
- log_verbose "Updating tracking.json (task $task_id, complete=$complete)..."
788
+ log_verbose "Setting up output capture..."
667
789
 
668
- local status="pending"
669
- if [[ "$complete" == "true" ]]; then
670
- status="complete"
671
- fi
790
+ # Create timestamped output directory in /tmp
791
+ local timestamp=$(date +"%Y%m%d_%H%M%S")
792
+ local output_dir="/tmp/ralph-run-$timestamp"
672
793
 
673
- local tracking_json
674
- tracking_json=$(cat "$tracking_file")
794
+ mkdir -p "$output_dir"
795
+ log_info "Output directory: $output_dir"
675
796
 
676
- local updated_json
677
- updated_json=$(echo "$tracking_json" | jq --arg id "$task_id" --arg status "$status" '.tasks[$id] = {status: $status}' 2>/dev/null || echo '{"tasks":{}}')
797
+ # Store output directory path in Ralph directory for reference
798
+ echo "$output_dir" > "$ralph_dir/.output_dir"
678
799
 
679
- echo "$updated_json" > "$tracking_file"
800
+ echo "$output_dir"
680
801
  }
681
802
 
682
- update_task_status_atomic() {
803
+ cleanup_old_output() {
804
+ log_verbose "Cleaning up old Ralph output directories..."
805
+
806
+ # Keep last 3 Ralph output directories, delete older ones
807
+ find /tmp -type d -name "ralph-run-*" -mtime +7d 2>/dev/null | while read old_dir; do
808
+ log_verbose "Removing old output directory: $old_dir"
809
+ rm -rf "$old_dir"
810
+ done
811
+ }
812
+
813
+ execute_ralph_loop() {
683
814
  local change_dir="$1"
684
- local task_id="$2"
685
- local complete="$3"
686
- local tracking_file="$4"
815
+ local ralph_dir="$2"
816
+ local max_iterations="${3:-50}"
687
817
 
688
- log_verbose "Updating task status atomically..."
818
+ log_info "Starting Ralph Wiggum loop with open-ralph-wiggum..."
819
+ log_info "Max iterations: $max_iterations"
820
+ log_info "Change directory: $change_dir"
689
821
 
690
- local tasks_file="$change_dir/tasks.md"
691
- local tasks_backup="${tasks_file}.bak"
692
- local tracking_backup="${tracking_file}.bak"
822
+ if ! command -v ralph &> /dev/null; then
823
+ log_error "ralph CLI not found."
824
+ log_error "Please install open-ralph-wiggum: npm install -g @th0rgal/ralph-wiggum"
825
+ return 1
826
+ fi
693
827
 
694
- cp "$tasks_file" "$tasks_backup"
695
- cp "$tracking_file" "$tracking_backup"
828
+ local template_file="$ralph_dir/prompt-template.md"
696
829
 
697
- update_task_checkbox "$change_dir" "$task_id" "$complete"
698
- update_tracking "$tracking_file" "$task_id" "$complete"
830
+ # Clean up old output directories and setup new one
831
+ cleanup_old_output
832
+ local output_dir=$(setup_output_capture "$ralph_dir")
699
833
 
700
- if [[ ! -f "$tasks_file" ]] || [[ ! -f "$tracking_file" ]]; then
701
- log_error "Update failed: files not found"
702
- mv "$tasks_backup" "$tasks_file"
703
- mv "$tracking_backup" "$tracking_file"
704
- return 1
834
+ sync_tasks_to_ralph "$change_dir" "$ralph_dir"
835
+ create_prompt_template "$change_dir" "$template_file"
836
+
837
+ # Generate PRD and write to file
838
+ local prd_content
839
+ prd_content=$(generate_prd "$change_dir")
840
+ echo "$prd_content" > "$ralph_dir/PRD.md"
841
+
842
+ # Get current task context for Ralph to use in commits
843
+ local task_context
844
+ task_context=$(get_current_task_context "$change_dir")
845
+
846
+ # Create context injection file for Ralph
847
+ local context_file="$ralph_dir/.context_injection"
848
+ if [[ -n "$task_context" ]]; then
849
+ log_verbose "Writing task context injection..."
850
+ echo "$task_context" > "$context_file"
705
851
  fi
706
852
 
707
- rm "$tasks_backup"
708
- rm "$tracking_backup"
853
+ # Restore Ralph state from tasks.md before running
854
+ restore_ralph_state_from_tasks "$change_dir/tasks.md"
709
855
 
710
- log_verbose "Atomic update successful"
711
- return 0
856
+ # Initialize context injections file for Ralph to read context
857
+ initialize_context_injections "$ralph_dir"
858
+
859
+ # Output files
860
+ local stdout_log="$output_dir/ralph-stdout.log"
861
+ local stderr_log="$output_dir/ralph-stderr.log"
862
+
863
+ log_info "Delegating to ralph CLI..."
864
+ log_info "Capturing output to: $output_dir"
865
+
866
+ # Run Ralph and capture output to both console and files
867
+ {
868
+ ralph --prompt-file "$ralph_dir/PRD.md" \
869
+ --agent opencode \
870
+ --tasks \
871
+ --max-iterations "$max_iterations" \
872
+ --prompt-template "$template_file" \
873
+ --verbose-tools
874
+ } > >(tee "$stdout_log") 2> >(tee "$stderr_log")
875
+
876
+ return $?
712
877
  }
713
878
 
879
+
880
+
714
881
  main() {
715
882
  parse_arguments "$@"
716
-
883
+
717
884
  log_verbose "Starting ralph-run v$VERSION"
718
885
  log_verbose "Change name: ${CHANGE_NAME:-<auto-detect>}"
719
-
886
+
720
887
  validate_git_repository
721
888
  validate_dependencies
722
889
 
@@ -729,7 +896,6 @@ main() {
729
896
  validate_openspec_artifacts "$change_dir"
730
897
  validate_script_state "$change_dir"
731
898
  local ralph_dir=$(setup_ralph_directory "$change_dir")
732
- local tracking_file=$(initialize_tracking "$ralph_dir")
733
899
 
734
900
  log_info "Change directory: $change_dir"
735
901
  log_info "Ralph directory: $ralph_dir"
@@ -743,7 +909,9 @@ main() {
743
909
  log_info "PRD generation complete"
744
910
  log_info "Found ${#TASKS[@]} tasks to execute"
745
911
 
746
- execute_task_loop "$ralph_dir"
912
+ local max_iterations="${MAX_ITERATIONS:-50}"
913
+
914
+ execute_ralph_loop "$change_dir" "$ralph_dir" "$max_iterations"
747
915
 
748
916
  log_info "ralph-run.sh initialized successfully"
749
917
  }