spec-and-loop 1.0.8 → 2.0.0-rc.1

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"
@@ -692,7 +705,7 @@ sync_tasks_to_ralph() {
692
705
  rm "$old_ralph_tasks_file"
693
706
  fi
694
707
 
695
- # Use symlink so Ralph and openspec-apply-change work on the SAME file
708
+ # Use a symlink so the loop runtime always works against the OpenSpec tasks file
696
709
  # Ensure parent directory for ralph_tasks_file exists
697
710
  mkdir -p "$(dirname "$ralph_tasks_file")"
698
711
 
@@ -744,6 +757,10 @@ Include full context from openspec artifacts in {{change_dir}}:
744
757
 
745
758
  {{tasks}}
746
759
 
760
+ ## Fresh Task Context
761
+
762
+ {{task_context}}
763
+
747
764
  ## Instructions
748
765
 
749
766
  1. **Identify** current task:
@@ -751,15 +768,15 @@ Include full context from openspec artifacts in {{change_dir}}:
751
768
  - If no task is in progress, pick the first task marked as [ ] (incomplete)
752
769
  - Mark the task as [/] in the tasks file before starting work
753
770
 
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
771
+ 2. **Implement** the current task directly:
772
+ - Read the relevant OpenSpec artifacts for context (proposal.md, design.md, specs)
773
+ - Make the smallest maintainable change that fully satisfies the current task
774
+ - Run the most relevant validation or tests for the task before claiming completion
759
775
 
760
776
  3. **Complete** task:
761
777
  - Verify that the implementation meets the requirements
762
778
  - When the task is successfully completed, mark it as [x] in the tasks file
779
+ - Create a git commit using the required format below
763
780
  - Output: `<promise>{{task_promise}}</promise>`
764
781
 
765
782
  4. **Continue** to the next task:
@@ -769,7 +786,9 @@ Include full context from openspec artifacts in {{change_dir}}:
769
786
  ## Critical Rules
770
787
 
771
788
  - Work on ONE task at a time from the task list
772
- - Use openspec-apply-change (/opsx-apply) for implementation
789
+ - Read the full tasks file every iteration; do not rely on memory from prior iterations
790
+ - Do not rely on editor-specific slash commands or local-only skills; follow this prompt directly
791
+ - Treat tasks.md as the only source of truth for task state
773
792
  - ONLY output `<promise>{{task_promise}}</promise>` when the current task is complete and marked as [x]
774
793
  - ONLY output `<promise>{{completion_promise}}</promise>` when ALL tasks are [x]
775
794
  - Output promise tags DIRECTLY - do not quote them, explain them, or say you "will" output them
@@ -881,12 +900,12 @@ get_current_task_context() {
881
900
  while IFS= read -r line; do
882
901
  if [[ "$line" =~ ^-\ \[/\] ]]; then
883
902
  # Found in-progress task - extract description
884
- current_task_desc="${line#- [/] }"
903
+ current_task_desc="${line#- \[/\] }"
885
904
  found_task=true
886
905
  break
887
906
  elif [[ "$line" =~ ^-\ \[\ \] ]] && [[ "$found_task" == "false" ]]; then
888
907
  # Found incomplete task - extract description
889
- current_task_desc="${line#- [ ] }"
908
+ current_task_desc="${line#- \[ \] }"
890
909
  found_task=true
891
910
  break
892
911
  fi
@@ -948,14 +967,18 @@ execute_ralph_loop() {
948
967
  local change_dir="$1"
949
968
  local ralph_dir="$2"
950
969
  local max_iterations="${3:-50}"
970
+ local no_commit="${4:-false}"
951
971
 
952
- log_info "Starting Ralph Wiggum loop with open-ralph-wiggum..."
972
+ log_info "Starting internal mini Ralph loop..."
953
973
  log_info "Max iterations: $max_iterations"
954
974
  log_info "Change directory: $change_dir"
975
+ if [[ "$no_commit" == true ]]; then
976
+ log_info "Auto-commit disabled (--no-commit)"
977
+ fi
955
978
 
956
979
  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"
980
+ log_error "Internal mini Ralph runtime not found: $MINI_RALPH_CLI"
981
+ log_error "Ensure node is installed and spec-and-loop dependencies are up to date (npm install)."
959
982
  return 1
960
983
  fi
961
984
 
@@ -973,51 +996,132 @@ execute_ralph_loop() {
973
996
  prd_content=$(generate_prd "$change_dir")
974
997
  echo "$prd_content" > "$ralph_dir/PRD.md"
975
998
 
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
999
  # Output files
994
1000
  local stdout_log="$output_dir/ralph-stdout.log"
995
1001
  local stderr_log="$output_dir/ralph-stderr.log"
996
1002
 
997
- log_info "Delegating to ralph CLI..."
1003
+ log_info "Invoking internal mini Ralph runtime..."
998
1004
  log_info "Capturing output to: $output_dir"
999
1005
 
1000
- # Run Ralph and capture output to both console and files
1006
+ # Build the mini-ralph-cli arguments
1007
+ local mini_ralph_args=(
1008
+ "--prompt-file" "$ralph_dir/PRD.md"
1009
+ "--prompt-template" "$template_file"
1010
+ "--ralph-dir" "$ralph_dir"
1011
+ "--tasks-file" "$change_dir/tasks.md"
1012
+ "--tasks"
1013
+ "--max-iterations" "$max_iterations"
1014
+ )
1015
+
1016
+ if [[ "$no_commit" == true ]]; then
1017
+ mini_ralph_args+=("--no-commit")
1018
+ fi
1019
+
1020
+ if [[ "$VERBOSE" == true ]]; then
1021
+ mini_ralph_args+=("--verbose")
1022
+ fi
1023
+
1024
+ # Run the internal mini Ralph CLI and capture output
1001
1025
  {
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
1026
+ node "$MINI_RALPH_CLI" "${mini_ralph_args[@]}"
1008
1027
  } > >(tee "$stdout_log") 2> >(tee "$stderr_log")
1009
1028
 
1010
1029
  return $?
1011
1030
  }
1012
1031
 
1013
1032
 
1033
+ # ---------------------------------------------------------------------------
1034
+ # Observability and control commands
1035
+ #
1036
+ # These commands delegate to the internal mini-ralph-cli.js for status,
1037
+ # context management, and other loop controls without running the full loop.
1038
+ # ---------------------------------------------------------------------------
1039
+
1040
+ run_observability_command() {
1041
+ local change_name="$1"
1042
+ local command="$2"
1043
+ local arg="$3"
1044
+
1045
+ if [[ ! -f "$MINI_RALPH_CLI" ]]; then
1046
+ log_error "Internal mini-ralph-cli.js not found: $MINI_RALPH_CLI"
1047
+ exit 1
1048
+ fi
1049
+
1050
+ if [[ ! -x "$(command -v node)" ]]; then
1051
+ log_error "node is required but not found in PATH."
1052
+ exit 1
1053
+ fi
1054
+
1055
+ local change_dir="openspec/changes/$change_name"
1056
+ local ralph_dir="$change_dir/.ralph"
1057
+
1058
+ case "$command" in
1059
+ status)
1060
+ local tasks_file="$change_dir/tasks.md"
1061
+ local tasks_arg=""
1062
+ if [[ -f "$tasks_file" ]]; then
1063
+ tasks_arg="--tasks-file $tasks_file"
1064
+ fi
1065
+ # shellcheck disable=SC2086
1066
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --status $tasks_arg
1067
+ ;;
1068
+ add-context)
1069
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --add-context "$arg"
1070
+ ;;
1071
+ clear-context)
1072
+ node "$MINI_RALPH_CLI" --ralph-dir "$ralph_dir" --clear-context
1073
+ ;;
1074
+ *)
1075
+ log_error "Unknown observability command: $command"
1076
+ exit 1
1077
+ ;;
1078
+ esac
1079
+ }
1014
1080
 
1015
1081
  main() {
1082
+ set -e
1016
1083
  parse_arguments "$@"
1017
1084
 
1018
1085
  log_verbose "Starting ralph-run v$VERSION"
1019
1086
  log_verbose "Change name: ${CHANGE_NAME:-<auto-detect>}"
1020
-
1087
+
1088
+ # Resolve change name first for observability commands that need it
1089
+ if [[ -z "$CHANGE_NAME" ]] && ( [[ "$SHOW_STATUS" == true ]] || [[ -n "$ADD_CONTEXT" ]] || [[ "$CLEAR_CONTEXT" == true ]] ); then
1090
+ validate_git_repository
1091
+ CHANGE_NAME=$(auto_detect_change)
1092
+ log_verbose "Auto-detected change for observability: $CHANGE_NAME"
1093
+ fi
1094
+
1095
+ # Handle observability commands (status, add-context, clear-context)
1096
+ # These exit early without running the full loop.
1097
+ if [[ "$SHOW_STATUS" == true ]]; then
1098
+ if [[ -z "$CHANGE_NAME" ]]; then
1099
+ validate_git_repository
1100
+ CHANGE_NAME=$(auto_detect_change)
1101
+ fi
1102
+ run_observability_command "$CHANGE_NAME" "status"
1103
+ exit $?
1104
+ fi
1105
+
1106
+ if [[ -n "$ADD_CONTEXT" ]]; then
1107
+ if [[ -z "$CHANGE_NAME" ]]; then
1108
+ validate_git_repository
1109
+ CHANGE_NAME=$(auto_detect_change)
1110
+ fi
1111
+ run_observability_command "$CHANGE_NAME" "add-context" "$ADD_CONTEXT"
1112
+ exit $?
1113
+ fi
1114
+
1115
+ if [[ "$CLEAR_CONTEXT" == true ]]; then
1116
+ if [[ -z "$CHANGE_NAME" ]]; then
1117
+ validate_git_repository
1118
+ CHANGE_NAME=$(auto_detect_change)
1119
+ fi
1120
+ run_observability_command "$CHANGE_NAME" "clear-context"
1121
+ exit $?
1122
+ fi
1123
+
1124
+ # Normal loop execution path
1021
1125
  validate_git_repository
1022
1126
  validate_dependencies
1023
1127
 
@@ -1045,9 +1149,11 @@ main() {
1045
1149
 
1046
1150
  local max_iterations="${MAX_ITERATIONS:-50}"
1047
1151
 
1048
- execute_ralph_loop "$change_dir" "$ralph_dir" "$max_iterations"
1152
+ execute_ralph_loop "$change_dir" "$ralph_dir" "$max_iterations" "$NO_COMMIT"
1049
1153
 
1050
1154
  log_info "ralph-run.sh initialized successfully"
1051
1155
  }
1052
1156
 
1053
- main "$@"
1157
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
1158
+ main "$@"
1159
+ 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 };