spec-and-loop 2.0.0-rc.1 → 2.0.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.
@@ -12,7 +12,6 @@
12
12
  */
13
13
 
14
14
  const { spawn, execFileSync } = require('child_process');
15
- const path = require('path');
16
15
 
17
16
  /**
18
17
  * Invoke OpenCode with the given prompt and return a result object.
@@ -22,7 +21,7 @@ const path = require('path');
22
21
  * @param {string} [opts.model] - Optional model override
23
22
  * @param {boolean} [opts.noCommit] - Skip git commit if true
24
23
  * @param {boolean} [opts.verbose] - Enable verbose output
25
- * @param {string} opts.ralphDir - .ralph/ directory (used for temp prompt file)
24
+ * @param {string} [opts.ralphDir] - Reserved for caller compatibility
26
25
  * @returns {Promise<{stdout: string, exitCode: number, toolUsage: Array, filesChanged: Array}>}
27
26
  */
28
27
  async function invoke(opts) {
@@ -31,52 +30,47 @@ async function invoke(opts) {
31
30
  model,
32
31
  noCommit = false,
33
32
  verbose = false,
34
- ralphDir,
35
33
  } = opts;
36
34
 
37
- // Write the prompt to a temp file to avoid shell escaping issues
38
- const fs = require('fs');
39
- const os = require('os');
40
- const tmpPromptFile = path.join(
41
- os.tmpdir(),
42
- `ralph-prompt-${Date.now()}-${process.pid}.md`
43
- );
35
+ if (!prompt || !prompt.trim()) {
36
+ throw new Error('mini-ralph invoker: prompt is empty');
37
+ }
44
38
 
45
- try {
46
- fs.writeFileSync(tmpPromptFile, prompt, 'utf8');
39
+ const args = ['run'];
40
+ if (model) {
41
+ args.push('--model', model);
42
+ }
43
+ args.push(prompt);
47
44
 
48
- const args = ['--print', tmpPromptFile];
49
- if (model) {
50
- args.push('--model', model);
51
- }
45
+ if (verbose) {
46
+ process.stderr.write(
47
+ `[mini-ralph] invoking: opencode ${args.slice(0, -1).join(' ')} <prompt>\n`
48
+ );
49
+ }
52
50
 
53
- if (verbose) {
54
- process.stderr.write(`[mini-ralph] invoking: opencode ${args.join(' ')}\n`);
55
- }
51
+ // Snapshot git-tracked files before invocation for file-change detection
52
+ const preSnapshot = _gitSnapshot();
56
53
 
57
- // Snapshot git-tracked files before invocation for file-change detection
58
- const preSnapshot = _gitSnapshot();
59
-
60
- const result = await _spawnOpenCode(args, verbose);
61
-
62
- // Detect which files changed during this iteration
63
- const postSnapshot = _gitSnapshot();
64
- const filesChanged = _diffSnapshots(preSnapshot, postSnapshot);
65
-
66
- return {
67
- stdout: result.stdout,
68
- exitCode: result.exitCode,
69
- toolUsage: _extractToolUsage(result.stdout),
70
- filesChanged,
71
- };
72
- } finally {
73
- // Clean up temp prompt file
74
- try {
75
- fs.unlinkSync(tmpPromptFile);
76
- } catch {
77
- // ignore cleanup errors
78
- }
54
+ const result = await _spawnOpenCode(args, verbose);
55
+ const combinedOutput = [result.stdout, result.stderr].filter(Boolean).join('\n');
56
+
57
+ if (_looksLikeCliHelp(combinedOutput)) {
58
+ throw new Error(
59
+ 'mini-ralph invoker: opencode printed CLI help instead of running the prompt. ' +
60
+ 'The installed opencode CLI is likely incompatible with this version of spec-and-loop.'
61
+ );
79
62
  }
63
+
64
+ // Detect which files changed during this iteration
65
+ const postSnapshot = _gitSnapshot();
66
+ const filesChanged = _diffSnapshots(preSnapshot, postSnapshot);
67
+
68
+ return {
69
+ stdout: result.stdout,
70
+ exitCode: result.exitCode,
71
+ toolUsage: _extractToolUsage(result.stdout),
72
+ filesChanged,
73
+ };
80
74
  }
81
75
 
82
76
  /**
@@ -122,6 +116,25 @@ function _spawnOpenCode(args, verbose) {
122
116
  });
123
117
  }
124
118
 
119
+ /**
120
+ * Detect whether OpenCode printed its CLI help banner instead of executing the
121
+ * requested prompt. This usually means the invocation contract drifted.
122
+ *
123
+ * @param {string} text
124
+ * @returns {boolean}
125
+ */
126
+ function _looksLikeCliHelp(text) {
127
+ if (!text) return false;
128
+
129
+ const hasHelpSections = text.includes('Commands:') && text.includes('Options:');
130
+ const hasOpenCodeUsage =
131
+ text.includes('opencode [project]') ||
132
+ text.includes('opencode run [message..]') ||
133
+ text.includes('run opencode with a message');
134
+
135
+ return hasHelpSections && hasOpenCodeUsage;
136
+ }
137
+
125
138
  /**
126
139
  * Extract a compact tool usage summary from OpenCode output.
127
140
  * Returns an array of { tool, count } objects.
@@ -202,4 +215,11 @@ function _diffSnapshots(preSnapshot, postSnapshot) {
202
215
  return changed;
203
216
  }
204
217
 
205
- module.exports = { invoke, _spawnOpenCode, _extractToolUsage, _gitSnapshot, _diffSnapshots };
218
+ module.exports = {
219
+ invoke,
220
+ _spawnOpenCode,
221
+ _looksLikeCliHelp,
222
+ _extractToolUsage,
223
+ _gitSnapshot,
224
+ _diffSnapshots,
225
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spec-and-loop",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0",
4
4
  "description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
5
5
  "main": "index.js",
6
6
  "bin": {