spec-and-loop 1.0.8 → 1.2.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.
@@ -1,14 +1,7 @@
1
1
  #!/bin/bash
2
2
 
3
- set -e
4
-
5
3
  VERSION="1.0.0"
6
4
 
7
- # Add Bun to PATH if installed
8
- if [[ -d "$HOME/.bun/bin" ]]; then
9
- export PATH="$HOME/.bun/bin:$PATH"
10
- fi
11
-
12
5
  # Detect OS for cross-platform compatibility
13
6
  detect_os() {
14
7
  case "$(uname -s)" in
@@ -63,8 +56,8 @@ get_realpath() {
63
56
 
64
57
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
65
58
  LOCAL_NODE_BIN="$SCRIPT_DIR/../node_modules/.bin"
66
- BUNDLED_RALPH_JS="$SCRIPT_DIR/../node_modules/@th0rgal/ralph-wiggum/bin/ralph.js"
67
- RALPH_CMD=()
59
+ # Allow tests to inject a mock by setting MINI_RALPH_CLI_OVERRIDE in the environment
60
+ MINI_RALPH_CLI="${MINI_RALPH_CLI_OVERRIDE:-$SCRIPT_DIR/mini-ralph-cli.js}"
68
61
 
69
62
  if [[ -d "$LOCAL_NODE_BIN" ]]; then
70
63
  case ":$PATH:" in
@@ -105,22 +98,18 @@ make_temp_dir() {
105
98
  }
106
99
 
107
100
  resolve_ralph_command() {
108
- if command -v ralph >/dev/null 2>&1; then
109
- RALPH_CMD=("ralph")
101
+ if [[ -f "$MINI_RALPH_CLI" ]] && command -v node >/dev/null 2>&1; then
110
102
  return 0
111
103
  fi
112
-
113
- if [[ -f "$BUNDLED_RALPH_JS" ]] && command -v node >/dev/null 2>&1; then
114
- RALPH_CMD=("node" "$BUNDLED_RALPH_JS")
115
- return 0
116
- fi
117
-
118
- RALPH_CMD=()
119
104
  return 1
120
105
  }
121
106
 
122
107
  CHANGE_NAME=""
123
108
  MAX_ITERATIONS=""
109
+ NO_COMMIT=false
110
+ SHOW_STATUS=false
111
+ ADD_CONTEXT=""
112
+ CLEAR_CONTEXT=false
124
113
  ERROR_OCCURRED=false
125
114
  CLEANUP_IN_PROGRESS=false
126
115
 
@@ -135,12 +124,10 @@ cleanup() {
135
124
  local exit_code=$1
136
125
  log_info "Cleaning up..."
137
126
 
138
- # NOTE: We do NOT kill ralph/bun processes here because:
139
- # 1. Ralph runs synchronously in the foreground
127
+ # NOTE: We do NOT kill node processes here because:
128
+ # 1. The mini Ralph runtime runs synchronously in the foreground
140
129
  # 2. Ctrl+C (SIGINT) naturally propagates to child processes
141
- # 3. Using pkill -f "bun" is DANGEROUS - it matches gnome-session-binary!
142
- # 4. Using pkill -f "ralph" could kill other user processes
143
- # The shell's process group handling ensures clean termination.
130
+ # 3. The shell's process group handling ensures clean termination.
144
131
 
145
132
  if [[ $exit_code -ne 0 ]]; then
146
133
  log_error "Script terminated with exit code: $exit_code"
@@ -148,7 +135,9 @@ cleanup() {
148
135
  exit $exit_code
149
136
  }
150
137
 
151
- trap 'cleanup $?' EXIT INT TERM QUIT
138
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
139
+ trap 'cleanup $?' EXIT INT TERM QUIT
140
+ fi
152
141
 
153
142
  handle_error() {
154
143
  local exit_code=$1
@@ -171,19 +160,28 @@ USAGE:
171
160
  OPTIONS:
172
161
  --change <name> Specify the OpenSpec change to execute (default: auto-detect)
173
162
  --max-iterations <n> Maximum iterations for Ralph loop (default: 50)
163
+ --no-commit Suppress automatic git commits during the loop
174
164
  --verbose, -v Enable verbose mode for debugging
175
165
  --help, -h Show this help message
176
166
 
167
+ OBSERVABILITY AND CONTROL:
168
+ --status Print the current loop status dashboard and exit
169
+ --add-context <text> Add pending context to inject into the next iteration and exit
170
+ --clear-context Clear any pending context and exit
171
+
177
172
  EXAMPLES:
178
173
  ralph-run # Auto-detect most recent change
179
174
  ralph-run --change my-feature # Execute specific change
180
- ralph-run --max-iterations 100 # Limit loop to 100 iterations
175
+ ralph-run --change my-feature --max-iterations 100
176
+ ralph-run --change my-feature --no-commit # Run without auto-committing
181
177
  ralph-run --verbose # Run with debug output
178
+ ralph-run --status # Check status of the active loop
179
+ ralph-run --add-context "Focus on error handling in module X"
180
+ ralph-run --clear-context # Remove pending context
182
181
 
183
182
  PREREQUISITES:
184
183
  - Git repository (git init)
185
- - OpenSpec artifacts created (openspec init, opsx-new, opsx-ff)
186
- - ralph CLI installed (npm install -g @th0rgal/ralph-wiggum)
184
+ - OpenSpec artifacts created (openspec init, openspec new, openspec ff)
187
185
  - opencode CLI installed (npm install -g opencode-ai)
188
186
 
189
187
  EOF
@@ -200,10 +198,26 @@ parse_arguments() {
200
198
  MAX_ITERATIONS="$2"
201
199
  shift 2
202
200
  ;;
201
+ --no-commit)
202
+ NO_COMMIT=true
203
+ shift
204
+ ;;
203
205
  --verbose|-v)
204
206
  VERBOSE=true
205
207
  shift
206
208
  ;;
209
+ --status)
210
+ SHOW_STATUS=true
211
+ shift
212
+ ;;
213
+ --add-context)
214
+ ADD_CONTEXT="$2"
215
+ shift 2
216
+ ;;
217
+ --clear-context)
218
+ CLEAR_CONTEXT=true
219
+ shift
220
+ ;;
207
221
  --help|-h)
208
222
  SHOW_HELP=true
209
223
  shift
@@ -252,12 +266,11 @@ validate_dependencies() {
252
266
  log_verbose "Validating dependencies..."
253
267
 
254
268
  if ! resolve_ralph_command; then
255
- log_error "ralph CLI not found."
256
- log_error "Please install open-ralph-wiggum: npm install -g @th0rgal/ralph-wiggum"
257
- log_error "If spec-and-loop is globally installed, reinstalling it should also restore the bundled Ralph dependency."
269
+ log_error "Internal mini Ralph runtime not found: $MINI_RALPH_CLI"
270
+ log_error "Ensure node is installed and spec-and-loop dependencies are up to date (npm install)."
258
271
  exit 1
259
272
  fi
260
- log_verbose "Found Ralph command: ${RALPH_CMD[*]}"
273
+ log_verbose "Found internal mini Ralph runtime: $MINI_RALPH_CLI"
261
274
 
262
275
  # Check for opencode
263
276
  if ! command -v opencode &> /dev/null; then
@@ -454,7 +467,7 @@ parse_tasks() {
454
467
  ((line_number++)) || true
455
468
 
456
469
  if [[ "$line" == "- [ ]"* ]]; then
457
- local task_desc="${line#- [ ] }"
470
+ local task_desc="${line#- \[ \] }"
458
471
  TASKS+=("$task_desc")
459
472
  TASK_IDS+=("$line_number")
460
473
  log_verbose "Found incomplete task (line $line_number): $task_desc"
@@ -483,6 +496,10 @@ check_tasks_modified() {
483
496
  return 1
484
497
  }
485
498
 
499
+ # DEPRECATED: The following functions are superseded by lib/mini-ralph/errors.js.
500
+ # Do not add new callers. These will be removed in a future cleanup.
501
+ # See: lib/mini-ralph/errors.js for the current implementation.
502
+
486
503
  format_error_entry() {
487
504
  local task_id="$1"
488
505
  local task_description="$2"
@@ -692,7 +709,7 @@ sync_tasks_to_ralph() {
692
709
  rm "$old_ralph_tasks_file"
693
710
  fi
694
711
 
695
- # Use symlink so Ralph and openspec-apply-change work on the SAME file
712
+ # Use a symlink so the loop runtime always works against the OpenSpec tasks file
696
713
  # Ensure parent directory for ralph_tasks_file exists
697
714
  mkdir -p "$(dirname "$ralph_tasks_file")"
698
715
 
@@ -744,6 +761,10 @@ Include full context from openspec artifacts in {{change_dir}}:
744
761
 
745
762
  {{tasks}}
746
763
 
764
+ ## Fresh Task Context
765
+
766
+ {{task_context}}
767
+
747
768
  ## Instructions
748
769
 
749
770
  1. **Identify** current task:
@@ -751,15 +772,15 @@ Include full context from openspec artifacts in {{change_dir}}:
751
772
  - If no task is in progress, pick the first task marked as [ ] (incomplete)
752
773
  - Mark the task as [/] in the tasks file before starting work
753
774
 
754
- 2. **Implement** task using openspec-apply-change:
755
- - Use the /opsx-apply skill to implement the current task
756
- - Read the relevant openspec artifacts for context (proposal.md, design.md, specs)
757
- - Follow the openspec workflow to complete the task
758
- - The openspec-apply-change skill will implement changes and update task status automatically
775
+ 2. **Implement** the current task directly:
776
+ - Read the relevant OpenSpec artifacts for context (proposal.md, design.md, specs)
777
+ - Make the smallest maintainable change that fully satisfies the current task
778
+ - Run the most relevant validation or tests for the task before claiming completion
759
779
 
760
780
  3. **Complete** task:
761
781
  - Verify that the implementation meets the requirements
762
782
  - When the task is successfully completed, mark it as [x] in the tasks file
783
+ - Create a git commit using the required format below
763
784
  - Output: `<promise>{{task_promise}}</promise>`
764
785
 
765
786
  4. **Continue** to the next task:
@@ -769,7 +790,9 @@ Include full context from openspec artifacts in {{change_dir}}:
769
790
  ## Critical Rules
770
791
 
771
792
  - Work on ONE task at a time from the task list
772
- - Use openspec-apply-change (/opsx-apply) for implementation
793
+ - Read the full tasks file every iteration; do not rely on memory from prior iterations
794
+ - Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
795
+ - Treat tasks.md as the only source of truth for task state
773
796
  - ONLY output `<promise>{{task_promise}}</promise>` when the current task is complete and marked as [x]
774
797
  - ONLY output `<promise>{{completion_promise}}</promise>` when ALL tasks are [x]
775
798
  - Output promise tags DIRECTLY - do not quote them, explain them, or say you "will" output them
@@ -881,12 +904,12 @@ get_current_task_context() {
881
904
  while IFS= read -r line; do
882
905
  if [[ "$line" =~ ^-\ \[/\] ]]; then
883
906
  # Found in-progress task - extract description
884
- current_task_desc="${line#- [/] }"
907
+ current_task_desc="${line#- \[/\] }"
885
908
  found_task=true
886
909
  break
887
910
  elif [[ "$line" =~ ^-\ \[\ \] ]] && [[ "$found_task" == "false" ]]; then
888
911
  # Found incomplete task - extract description
889
- current_task_desc="${line#- [ ] }"
912
+ current_task_desc="${line#- \[ \] }"
890
913
  found_task=true
891
914
  break
892
915
  fi
@@ -948,14 +971,18 @@ execute_ralph_loop() {
948
971
  local change_dir="$1"
949
972
  local ralph_dir="$2"
950
973
  local max_iterations="${3:-50}"
974
+ local no_commit="${4:-false}"
951
975
 
952
- log_info "Starting Ralph Wiggum loop with open-ralph-wiggum..."
976
+ log_info "Starting internal mini Ralph loop..."
953
977
  log_info "Max iterations: $max_iterations"
954
978
  log_info "Change directory: $change_dir"
979
+ if [[ "$no_commit" == true ]]; then
980
+ log_info "Auto-commit disabled (--no-commit)"
981
+ fi
955
982
 
956
983
  if ! resolve_ralph_command; then
957
- log_error "ralph CLI not found."
958
- log_error "Please install open-ralph-wiggum: npm install -g @th0rgal/ralph-wiggum"
984
+ log_error "Internal mini Ralph runtime not found: $MINI_RALPH_CLI"
985
+ log_error "Ensure node is installed and spec-and-loop dependencies are up to date (npm install)."
959
986
  return 1
960
987
  fi
961
988
 
@@ -973,51 +1000,132 @@ execute_ralph_loop() {
973
1000
  prd_content=$(generate_prd "$change_dir")
974
1001
  echo "$prd_content" > "$ralph_dir/PRD.md"
975
1002
 
976
- # Get current task context for Ralph to use in commits
977
- local task_context
978
- task_context=$(get_current_task_context "$change_dir")
979
-
980
- # Create context injection file for Ralph
981
- local context_file="$ralph_dir/.context_injection"
982
- if [[ -n "$task_context" ]]; then
983
- log_verbose "Writing task context injection..."
984
- echo "$task_context" > "$context_file"
985
- fi
986
-
987
- # Restore Ralph state from tasks.md before running
988
- restore_ralph_state_from_tasks "$change_dir/tasks.md"
989
-
990
- # Initialize context injections file for Ralph to read context
991
- initialize_context_injections "$ralph_dir"
992
-
993
1003
  # Output files
994
1004
  local stdout_log="$output_dir/ralph-stdout.log"
995
1005
  local stderr_log="$output_dir/ralph-stderr.log"
996
1006
 
997
- log_info "Delegating to ralph CLI..."
1007
+ log_info "Invoking internal mini Ralph runtime..."
998
1008
  log_info "Capturing output to: $output_dir"
999
1009
 
1000
- # Run Ralph and capture output to both console and files
1010
+ # Build the mini-ralph-cli arguments
1011
+ local mini_ralph_args=(
1012
+ "--prompt-file" "$ralph_dir/PRD.md"
1013
+ "--prompt-template" "$template_file"
1014
+ "--ralph-dir" "$ralph_dir"
1015
+ "--tasks-file" "$change_dir/tasks.md"
1016
+ "--tasks"
1017
+ "--max-iterations" "$max_iterations"
1018
+ )
1019
+
1020
+ if [[ "$no_commit" == true ]]; then
1021
+ mini_ralph_args+=("--no-commit")
1022
+ fi
1023
+
1024
+ if [[ "$VERBOSE" == true ]]; then
1025
+ mini_ralph_args+=("--verbose")
1026
+ fi
1027
+
1028
+ # Run the internal mini Ralph CLI and capture output
1001
1029
  {
1002
- "${RALPH_CMD[@]}" --prompt-file "$ralph_dir/PRD.md" \
1003
- --agent opencode \
1004
- --tasks \
1005
- --max-iterations "$max_iterations" \
1006
- --prompt-template "$template_file" \
1007
- --verbose-tools
1030
+ node "$MINI_RALPH_CLI" "${mini_ralph_args[@]}"
1008
1031
  } > >(tee "$stdout_log") 2> >(tee "$stderr_log")
1009
1032
 
1010
1033
  return $?
1011
1034
  }
1012
1035
 
1013
1036
 
1037
+ # ---------------------------------------------------------------------------
1038
+ # Observability and control commands
1039
+ #
1040
+ # These commands delegate to the internal mini-ralph-cli.js for status,
1041
+ # context management, and other loop controls without running the full loop.
1042
+ # ---------------------------------------------------------------------------
1043
+
1044
+ run_observability_command() {
1045
+ local change_name="$1"
1046
+ local command="$2"
1047
+ local arg="$3"
1048
+
1049
+ if [[ ! -f "$MINI_RALPH_CLI" ]]; then
1050
+ log_error "Internal mini-ralph-cli.js not found: $MINI_RALPH_CLI"
1051
+ exit 1
1052
+ fi
1053
+
1054
+ if [[ ! -x "$(command -v node)" ]]; then
1055
+ log_error "node is required but not found in PATH."
1056
+ exit 1
1057
+ fi
1058
+
1059
+ local change_dir="openspec/changes/$change_name"
1060
+ local ralph_dir="$change_dir/.ralph"
1061
+
1062
+ case "$command" in
1063
+ status)
1064
+ local tasks_file="$change_dir/tasks.md"
1065
+ local tasks_arg=""
1066
+ if [[ -f "$tasks_file" ]]; then
1067
+ tasks_arg="--tasks-file $tasks_file"
1068
+ fi
1069
+ # shellcheck disable=SC2086
1070
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --status $tasks_arg
1071
+ ;;
1072
+ add-context)
1073
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --add-context "$arg"
1074
+ ;;
1075
+ clear-context)
1076
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --clear-context
1077
+ ;;
1078
+ *)
1079
+ log_error "Unknown observability command: $command"
1080
+ exit 1
1081
+ ;;
1082
+ esac
1083
+ }
1014
1084
 
1015
1085
  main() {
1086
+ set -e
1016
1087
  parse_arguments "$@"
1017
1088
 
1018
1089
  log_verbose "Starting ralph-run v$VERSION"
1019
1090
  log_verbose "Change name: ${CHANGE_NAME:-<auto-detect>}"
1020
-
1091
+
1092
+ # Resolve change name first for observability commands that need it
1093
+ if [[ -z "$CHANGE_NAME" ]] && ( [[ "$SHOW_STATUS" == true ]] || [[ -n "$ADD_CONTEXT" ]] || [[ "$CLEAR_CONTEXT" == true ]] ); then
1094
+ validate_git_repository
1095
+ CHANGE_NAME=$(auto_detect_change)
1096
+ log_verbose "Auto-detected change for observability: $CHANGE_NAME"
1097
+ fi
1098
+
1099
+ # Handle observability commands (status, add-context, clear-context)
1100
+ # These exit early without running the full loop.
1101
+ if [[ "$SHOW_STATUS" == true ]]; then
1102
+ if [[ -z "$CHANGE_NAME" ]]; then
1103
+ validate_git_repository
1104
+ CHANGE_NAME=$(auto_detect_change)
1105
+ fi
1106
+ run_observability_command "$CHANGE_NAME" "status"
1107
+ exit $?
1108
+ fi
1109
+
1110
+ if [[ -n "$ADD_CONTEXT" ]]; then
1111
+ if [[ -z "$CHANGE_NAME" ]]; then
1112
+ validate_git_repository
1113
+ CHANGE_NAME=$(auto_detect_change)
1114
+ fi
1115
+ run_observability_command "$CHANGE_NAME" "add-context" "$ADD_CONTEXT"
1116
+ exit $?
1117
+ fi
1118
+
1119
+ if [[ "$CLEAR_CONTEXT" == true ]]; then
1120
+ if [[ -z "$CHANGE_NAME" ]]; then
1121
+ validate_git_repository
1122
+ CHANGE_NAME=$(auto_detect_change)
1123
+ fi
1124
+ run_observability_command "$CHANGE_NAME" "clear-context"
1125
+ exit $?
1126
+ fi
1127
+
1128
+ # Normal loop execution path
1021
1129
  validate_git_repository
1022
1130
  validate_dependencies
1023
1131
 
@@ -1045,9 +1153,11 @@ main() {
1045
1153
 
1046
1154
  local max_iterations="${MAX_ITERATIONS:-50}"
1047
1155
 
1048
- execute_ralph_loop "$change_dir" "$ralph_dir" "$max_iterations"
1156
+ execute_ralph_loop "$change_dir" "$ralph_dir" "$max_iterations" "$NO_COMMIT"
1049
1157
 
1050
1158
  log_info "ralph-run.sh initialized successfully"
1051
1159
  }
1052
1160
 
1053
- main "$@"
1161
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1162
+ main "$@"
1163
+ fi
package/scripts/setup.js CHANGED
@@ -4,40 +4,48 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { execSync } = require('child_process');
6
6
 
7
- console.log('Setting up spec-and-loop...');
7
+ function runSetup() {
8
+ console.log('Setting up spec-and-loop...');
8
9
 
9
- // Get the installation directory
10
- const installDir = __dirname;
11
- const ralphRunScript = path.join(installDir, 'ralph-run.sh');
10
+ // Get the installation directory
11
+ const installDir = __dirname;
12
+ const ralphRunScript = path.join(installDir, 'ralph-run.sh');
12
13
 
13
- console.log(`Installation directory: ${installDir}`);
14
+ console.log(`Installation directory: ${installDir}`);
14
15
 
15
- // Make ralph-run.sh executable
16
- if (fs.existsSync(ralphRunScript)) {
17
- try {
18
- execSync(`chmod +x "${ralphRunScript}"`);
19
- console.log('✓ Made ralph-run.sh executable');
20
- } catch (err) {
21
- console.warn(`Could not make ralph-run.sh executable: ${err.message}`);
16
+ // Make ralph-run.sh executable
17
+ if (fs.existsSync(ralphRunScript)) {
18
+ try {
19
+ execSync(`chmod +x "${ralphRunScript}"`, { stdio: 'inherit' });
20
+ console.log('✓ Made ralph-run.sh executable');
21
+ } catch (err) {
22
+ console.warn(`Could not make ralph-run.sh executable: ${err.message}`);
23
+ }
24
+ } else {
25
+ console.error(`Error: ralph-run.sh not found at ${ralphRunScript}`);
26
+ process.exit(1);
22
27
  }
23
- } else {
24
- console.error(`Error: ralph-run.sh not found at ${ralphRunScript}`);
25
- process.exit(1);
28
+
29
+ console.log('');
30
+ console.log('spec-and-loop setup complete!');
31
+ console.log('');
32
+ console.log('Usage:');
33
+ console.log(' cd /path/to/your/project');
34
+ console.log(' openspec init # Initialize OpenSpec');
35
+ console.log(' openspec new <name> # Create a new change');
36
+ console.log(' openspec ff <name> # Fast-forward artifacts');
37
+ console.log(' ralph-run --change <name> # Run ralph loop');
38
+ console.log(' ralph-run # Auto-detect change');
39
+ console.log('');
40
+ console.log('Prerequisites:');
41
+ console.log(' - openspec CLI: npm install -g openspec');
42
+ console.log(' - opencode CLI: npm install -g opencode-ai');
43
+ console.log(' - jq CLI: apt install jq / brew install jq');
44
+ console.log(' - git: git init');
45
+ }
46
+
47
+ if (require.main === module) {
48
+ runSetup();
26
49
  }
27
50
 
28
- console.log('');
29
- console.log('spec-and-loop setup complete!');
30
- console.log('');
31
- console.log('Usage:');
32
- console.log(' cd /path/to/your/project');
33
- console.log(' openspec init # Initialize OpenSpec');
34
- console.log(' openspec new <name> # Create a new change');
35
- console.log(' openspec ff <name> # Fast-forward artifacts');
36
- console.log(' ralph-run --change <name> # Run ralph loop');
37
- console.log(' ralph-run # Auto-detect change');
38
- console.log('');
39
- console.log('Prerequisites:');
40
- console.log(' - openspec CLI: npm install -g openspec');
41
- console.log(' - opencode CLI: npm install -g opencode-ai');
42
- console.log(' - jq CLI: apt install jq / brew install jq');
43
- console.log(' - git: git init');
51
+ module.exports = { runSetup };