spec-and-loop 3.1.0 → 3.3.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.
@@ -60,6 +60,31 @@ function render(ralphDir, tasksFile) {
60
60
  lines.push(`Exit reason: ${loopState.exitReason}`);
61
61
  }
62
62
 
63
+ const pendingDirtyPaths = _pendingDirtyPaths(loopState);
64
+ if (pendingDirtyPaths) {
65
+ lines.push('');
66
+ lines.push('--- Pending Dirty Paths ---');
67
+ lines.push(` Reason: ${pendingDirtyPaths.reason || 'blocked_handoff'}`);
68
+ if (pendingDirtyPaths.iteration) {
69
+ lines.push(` From iteration: ${pendingDirtyPaths.iteration}`);
70
+ }
71
+ const task = pendingDirtyPaths.taskNumber
72
+ ? `${pendingDirtyPaths.taskNumber} ${pendingDirtyPaths.taskDescription || ''}`.trim()
73
+ : (pendingDirtyPaths.task || '');
74
+ if (task) {
75
+ lines.push(` Prior task: ${task}`);
76
+ }
77
+ const files = pendingDirtyPaths.files.slice(0, 10);
78
+ for (const file of files) {
79
+ lines.push(` - ${file}`);
80
+ }
81
+ if (pendingDirtyPaths.files.length > files.length) {
82
+ lines.push(` - (+${pendingDirtyPaths.files.length - files.length} more)`);
83
+ }
84
+ lines.push(' Resolve before continuing: commit with the same task, revert, or move to a separate change.');
85
+ lines.push('-'.repeat(50));
86
+ }
87
+
63
88
  const latestCommitAnomaly = _latestCommitAnomaly(history.recent(ralphDir, 20));
64
89
  if (latestCommitAnomaly) {
65
90
  lines.push(`Commit issue: ${latestCommitAnomaly.commitAnomaly}`);
@@ -186,6 +211,16 @@ function _promptSummary(loopState) {
186
211
  return '';
187
212
  }
188
213
 
214
+ function _pendingDirtyPaths(loopState) {
215
+ const pending = loopState && loopState.pendingDirtyPaths;
216
+ if (!pending || typeof pending !== 'object') return null;
217
+ const files = Array.isArray(pending.files)
218
+ ? pending.files.filter((file) => typeof file === 'string' && file.trim())
219
+ : [];
220
+ if (files.length === 0) return null;
221
+ return Object.assign({}, pending, { files });
222
+ }
223
+
189
224
  /**
190
225
  * Try to find a tasks file path from loop state.
191
226
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-and-loop",
3
- "version": "3.1.0",
3
+ "version": "3.3.2",
4
4
  "description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -22,6 +22,11 @@
22
22
  * --stall-threshold <n> Halt after N consecutive no-op iterations (default: 3; 0 disables)
23
23
  * --completion-promise <s> Completion promise string (default: COMPLETE)
24
24
  * --task-promise <s> Task promise string (default: READY_FOR_NEXT_TASK)
25
+ * --blocked-handoff-promise <s>
26
+ * Blocked-handoff promise string (default: BLOCKED_HANDOFF).
27
+ * Loop exits cleanly with `blocked_handoff` when the
28
+ * agent emits this tag and writes the agent's note
29
+ * to <ralph-dir>/HANDOFF.md.
25
30
  * --no-commit Suppress auto-commit
26
31
  * --model <name> Optional model override
27
32
  * --verbose Verbose output
@@ -53,6 +58,7 @@ function parseArgs(argv) {
53
58
  stallThreshold: 3,
54
59
  completionPromise: 'COMPLETE',
55
60
  taskPromise: 'READY_FOR_NEXT_TASK',
61
+ blockedHandoffPromise: 'BLOCKED_HANDOFF',
56
62
  noCommit: false,
57
63
  model: '',
58
64
  verbose: false,
@@ -101,6 +107,9 @@ function parseArgs(argv) {
101
107
  case '--task-promise':
102
108
  opts.taskPromise = args[++i];
103
109
  break;
110
+ case '--blocked-handoff-promise':
111
+ opts.blockedHandoffPromise = args[++i];
112
+ break;
104
113
  case '--no-commit':
105
114
  opts.noCommit = true;
106
115
  break;
@@ -154,6 +163,8 @@ Options:
154
163
  --stall-threshold <n> Halt after N consecutive no-op iterations (default: 3; 0 disables)
155
164
  --completion-promise <s> Completion promise string
156
165
  --task-promise <s> Task promise string
166
+ --blocked-handoff-promise <s>
167
+ Blocked-handoff promise string (default: BLOCKED_HANDOFF)
157
168
  --no-commit Suppress auto-commit
158
169
  --model <name> Model override
159
170
  --verbose Verbose output
@@ -212,6 +223,7 @@ async function main() {
212
223
  stallThreshold: opts.stallThreshold,
213
224
  completionPromise: opts.completionPromise,
214
225
  taskPromise: opts.taskPromise,
226
+ blockedHandoffPromise: opts.blockedHandoffPromise,
215
227
  noCommit: opts.noCommit,
216
228
  model: opts.model,
217
229
  verbose: opts.verbose,
@@ -333,6 +333,19 @@ validate_dependencies() {
333
333
  log_verbose "All dependencies validated"
334
334
  }
335
335
 
336
+ should_auto_fix_artifacts() {
337
+ case "${RALPH_RUN_AUTO_FIX_ARTIFACTS:-}" in
338
+ 1|true|TRUE|yes|YES)
339
+ return 0
340
+ ;;
341
+ 0|false|FALSE|no|NO)
342
+ return 1
343
+ ;;
344
+ esac
345
+
346
+ [[ -t 0 ]]
347
+ }
348
+
336
349
  ensure_artifacts_present() {
337
350
  local change_dir="$1"
338
351
  local change_name="$2"
@@ -351,6 +364,13 @@ ensure_artifacts_present() {
351
364
  fi
352
365
 
353
366
  log_info "Blocked artifacts detected: $blocked"
367
+ if ! should_auto_fix_artifacts; then
368
+ log_error "OpenSpec artifacts are blocked and this is a non-interactive run."
369
+ log_error 'Run `ralph-run init` or complete the artifacts manually, then rerun.'
370
+ log_error "Set RALPH_RUN_AUTO_FIX_ARTIFACTS=true to opt into opencode artifact repair in automation."
371
+ exit 1
372
+ fi
373
+
354
374
  log_info "Invoking opencode to complete missing artifacts..."
355
375
 
356
376
  opencode run "/opsx-ff $change_name" || true
@@ -651,15 +671,21 @@ validate_script_state() {
651
671
 
652
672
  log_verbose "Validating script state..."
653
673
 
654
- local required_dirs=(
655
- ".ralph"
656
- )
657
-
658
- for dir in "${required_dirs[@]}"; do
659
- if [[ ! -d "$change_dir/$dir" ]]; then
660
- log_verbose "Required directory not found: $dir (will be created)"
661
- fi
662
- done
674
+ if [[ ! -d "$change_dir/.ralph" ]]; then
675
+ log_verbose "Required directory not found: .ralph (will be created)"
676
+ fi
677
+
678
+ if [[ ! -d "$change_dir/specs" ]]; then
679
+ log_error "Required directory not found: specs"
680
+ return 1
681
+ fi
682
+
683
+ local first_spec=""
684
+ first_spec=$(find "$change_dir/specs" -name "spec.md" -type f -print -quit 2>/dev/null || true)
685
+ if [[ -z "$first_spec" ]]; then
686
+ log_error "No spec.md files found under specs"
687
+ return 1
688
+ fi
663
689
 
664
690
  local required_files=(
665
691
  "tasks.md"
@@ -949,7 +975,20 @@ You are operating inside an automated loop. Follow these constraints EXACTLY:
949
975
  1. Implement exactly ONE pending task from the task list /opsx-apply shows you.
950
976
  2. After marking the task checkbox [x] on disk, output <promise>READY_FOR_NEXT_TASK</promise> on its own line.
951
977
  3. If and only if EVERY task checkbox is [x], output <promise>COMPLETE</promise> instead.
952
- 4. Do not ask questions or wait for input. If blocked, output a short failure note describing the blocker and stop.
978
+ 4. Do not ask questions or wait for input. If you cannot make progress on the current task because an external decision is required (revert protected drift outside the change scope, file an out-of-scope refactor, escalate to a human reviewer, etc.), STOP and emit a structured handoff in this exact form:
979
+
980
+ ## Blocker Note
981
+ <one paragraph describing what is blocked>
982
+
983
+ ## Why
984
+ <one paragraph: which task spec clause / invariant fired, and what evidence (file paths, hashes, test names) supports the diagnosis>
985
+
986
+ ## Suggested Next Step
987
+ <one or two bullets the human can execute to unblock>
988
+
989
+ <promise>BLOCKED_HANDOFF</promise>
990
+
991
+ The runner will save this note to .ralph/HANDOFF.md and exit cleanly with reason=blocked_handoff. Do NOT keep retrying the same task; emit the handoff and stop. Do NOT emit BLOCKED_HANDOFF for transient errors that a retry could fix (network blips, tool-not-found that is fixable by an absolute path, etc.) — those are normal failures the loop will retry on its own.
953
992
  5. If the task is already satisfied by prior work, still flip the checkbox to [x] before emitting the promise.
954
993
 
955
994
  Do not create git commits yourself. The Ralph runner manages automatic task commits when auto-commit is enabled."
@@ -975,12 +1014,53 @@ Do not create git commits yourself. The Ralph runner manages automatic task comm
975
1014
  mini_ralph_args+=("--quiet")
976
1015
  fi
977
1016
 
978
- # Run the internal mini Ralph CLI and capture output
979
- {
980
- node "$MINI_RALPH_CLI" "${mini_ralph_args[@]}"
981
- } > >(tee "$stdout_log") 2> >(tee "$stderr_log")
982
- local node_exit_code=$?
983
- wait
1017
+ # Run the internal mini Ralph CLI and capture output.
1018
+ #
1019
+ # Avoid Bash process substitution here. macOS ships Bash 3.2, and under
1020
+ # Bats' captured `run` wrapper a bare `wait` after `> >(tee ...)` can hang
1021
+ # after the node child has already exited. Explicit FIFOs give us concrete
1022
+ # tee PIDs to wait on and work consistently on macOS and Linux.
1023
+ local stdout_pipe="$output_dir/ralph-stdout.pipe"
1024
+ local stderr_pipe="$output_dir/ralph-stderr.pipe"
1025
+ local node_exit_code=0
1026
+ local tee_stdout_pid=""
1027
+ local tee_stderr_pid=""
1028
+ local had_errexit=false
1029
+ case $- in
1030
+ *e*)
1031
+ had_errexit=true
1032
+ set +e
1033
+ ;;
1034
+ esac
1035
+
1036
+ if mkfifo "$stdout_pipe" "$stderr_pipe" 2>/dev/null; then
1037
+ tee "$stdout_log" < "$stdout_pipe" &
1038
+ tee_stdout_pid=$!
1039
+ tee "$stderr_log" < "$stderr_pipe" >&2 &
1040
+ tee_stderr_pid=$!
1041
+
1042
+ node "$MINI_RALPH_CLI" "${mini_ralph_args[@]}" > "$stdout_pipe" 2> "$stderr_pipe"
1043
+ node_exit_code=$?
1044
+
1045
+ wait "$tee_stdout_pid" 2>/dev/null || true
1046
+ wait "$tee_stderr_pid" 2>/dev/null || true
1047
+ rm -f "$stdout_pipe" "$stderr_pipe"
1048
+ else
1049
+ log_verbose "mkfifo unavailable; capturing output without live tee"
1050
+ node "$MINI_RALPH_CLI" "${mini_ralph_args[@]}" > "$stdout_log" 2> "$stderr_log"
1051
+ node_exit_code=$?
1052
+ if [[ -s "$stdout_log" ]]; then
1053
+ cat "$stdout_log"
1054
+ fi
1055
+ if [[ -s "$stderr_log" ]]; then
1056
+ cat "$stderr_log" >&2
1057
+ fi
1058
+ fi
1059
+
1060
+ if [[ "$had_errexit" == true ]]; then
1061
+ set -e
1062
+ fi
1063
+
984
1064
  return $node_exit_code
985
1065
  }
986
1066
 
@@ -1102,6 +1182,7 @@ rules:
1102
1182
  tasks:
1103
1183
  - Use the task template from OPENSPEC-RALPH-BP.md
1104
1184
  - Each task has one dominant outcome and one verification cluster
1185
+ - Use surgical, scope-targeted validation commands; reserve broad gates for pre-flight baselines or final integration tasks
1105
1186
  - Include explicit stop-and-hand-off conditions
1106
1187
  design:
1107
1188
  - Do not leave core policy choices unresolved
@@ -1123,6 +1204,7 @@ Before generating any OpenSpec artifacts, you MUST:
1123
1204
  - Read `openspec/OPENSPEC-RALPH-BP.md` (Ralph Wiggum authoring guide)
1124
1205
  - Verify proposals against the Ralph authoring checklist
1125
1206
  - Ensure tasks use the task template with objective done-when conditions
1207
+ - Ensure each task uses the narrowest verifier that proves its scope; use broad gates only with baseline classification or final integration tasks
1126
1208
  - Include explicit stop-and-hand-off conditions in every task
1127
1209
  RALPH_AGENTS
1128
1210
  log_verbose "Updated $agents_file with Ralph Wiggum compliance section"
@@ -1159,6 +1241,7 @@ check_ralphified() {
1159
1241
 
1160
1242
  show_ralphify_warning() {
1161
1243
  local change_name="$1"
1244
+ local preset_choice="${RALPH_RUN_RALPHIFY_CHOICE:-}"
1162
1245
 
1163
1246
  cat >&2 << 'WARNING_BOX'
1164
1247
  ┌─────────────────────────────────────────────────────────────────────┐
@@ -1173,16 +1256,29 @@ show_ralphify_warning() {
1173
1256
  └─────────────────────────────────────────────────────────────────────┘
1174
1257
  WARNING_BOX
1175
1258
 
1259
+ if [[ -z "$preset_choice" && ! -t 0 ]]; then
1260
+ log_info "Non-interactive environment detected. Continuing without Ralph Wiggum configuration."
1261
+ log_info 'Run `ralph-run init` to configure Ralph Wiggum best practices before the next interactive run.'
1262
+ return 0
1263
+ fi
1264
+
1176
1265
  while true; do
1177
- echo "" >&2
1178
- echo "Choose an option:" >&2
1179
- echo " [A] Run ralphify init and redo the proposal, then continue" >&2
1180
- echo " [C] Continue without init" >&2
1181
- echo " [Q] Quit" >&2
1182
- printf "Enter choice: " >&2
1183
- if ! read -r choice; then
1184
- log_info "Non-interactive environment detected. Continuing without Ralph Wiggum configuration."
1185
- return 0
1266
+ local choice=""
1267
+ if [[ -n "$preset_choice" ]]; then
1268
+ choice="$preset_choice"
1269
+ preset_choice=""
1270
+ log_info "Using RALPH_RUN_RALPHIFY_CHOICE=$choice"
1271
+ else
1272
+ echo "" >&2
1273
+ echo "Choose an option:" >&2
1274
+ echo " [A] Run ralphify init and redo the proposal, then continue" >&2
1275
+ echo " [C] Continue without init" >&2
1276
+ echo " [Q] Quit" >&2
1277
+ printf "Enter choice: " >&2
1278
+ if ! read -r choice; then
1279
+ log_info "Non-interactive environment detected. Continuing without Ralph Wiggum configuration."
1280
+ return 0
1281
+ fi
1186
1282
  fi
1187
1283
 
1188
1284
  case "$choice" in
@@ -1217,7 +1313,7 @@ WARNING_BOX
1217
1313
  fi
1218
1314
  local ralph_guidance=""
1219
1315
  if [[ -f "$bp_file" ]]; then
1220
- ralph_guidance=" When creating artifacts, read ${bp_file} and follow the Ralph Wiggum task template and authoring checklist. Ensure the proposal includes explicit scope, non-goals, first-rollout boundaries, and capabilities that map to Ralph-friendly tasks. Ensure tasks use the task template with objective done-when conditions and explicit stop-and-hand-off conditions. Do NOT restore or copy from any .bak backup files - write fresh artifacts from scratch."
1316
+ ralph_guidance=" When creating artifacts, read ${bp_file} and follow the Ralph Wiggum task template and authoring checklist. Ensure the proposal includes explicit scope, non-goals, first-rollout boundaries, and capabilities that map to Ralph-friendly tasks. Ensure tasks use the task template with objective done-when conditions, surgical scope-targeted verifier commands, and explicit stop-and-hand-off conditions. Prefer direct test-file or validator commands over full-suite commands; reserve broad gates for pre-flight baselines or final integration tasks. Do NOT restore or copy from any .bak backup files - write fresh artifacts from scratch."
1221
1317
  fi
1222
1318
 
1223
1319
  log_info "Invoking opencode to regenerate proposal and tasks with Ralph Wiggum best practices..."